Repository files navigation

Rate Limiter Service

A small FastAPI service that enforces per-user request rate limits using a rolling 1-minute window.

How to run

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
fastapi dev

API docs available at http://127.0.0.1:8000/docs

How to package

zip -r submission.zip . -x "*.pyc" -x "*/__pycache__/*" -x ".venv/*" -x ".pytest_cache/*" -x "*.pdf" -x ".git/*" -x "*.zip"

How to test

pytest -v

The test suite covers:

  • User creation and auto-incrementing IDs
  • Request recording, remaining decrement, and independent quotas per user
  • Rate limiting at the 10 request limit with Retry-After header validation
  • Input validation: missing body, invalid types, and non-existent users
  • Quota endpoint covering fresh users, partial usage, and exhausted quota
  • Window reset behaviour: rather than sleeping 60 seconds, the time module inside store.py is patched to return a future timestamp, verifying the rolling window resets correctly without slowing the test suite down by using something like time.sleep()

Design Decisions

Why FastAPI

The assignment gave the choice between Django and FastAPI. I went with FastAPI because it's lighter and faster to set up. I also noticed from the job description that Sendcloud uses FastAPI, so it felt like the right fit. FastAPI's integration with Pydantic gives input validation and auto-generated OpenAPI docs out of the box, with the interactive docs available at /docs without any extra configuration. If the API grew, I'd add summary, description, and tags to each endpoint to keep the docs readable and useful.

Rolling Window Algorithm

Each user has a deque of request timestamps. When checking quota, I remove any timestamps older than 60 seconds from the front, then count what's left. If there are fewer than 10, the request goes through and the new timestamp gets appended.

I used a deque instead of a regular list because removing from the front is O(1) with popleft() vs O(n) with list.pop(0). At max 10 entries this doesn't really matter performance-wise, but it's the right data structure for the job.

Keeping User as a plain dataclass

I considered moving the window eviction logic into the User class itself, which would be more of a DDD approach. I decided against it because I wanted User to stay a simple data container and keep the business logic in the Store. For a service this small, mixing data and behaviour in the same class felt like unnecessary complexity.

User lookup at the endpoint layer

I call get_user in the endpoint rather than inside the store methods like record_request or get_quota. This keeps the store focused purely on business logic. Its methods assume they receive a valid User and don't need to handle the not-found case. The endpoint layer is the right place for HTTP concerns like returning a 404. If user lookup were inside the store, it would need to decide what to return when the user doesn't exist, which leaks HTTP-level concerns into the data layer.

get_quota reuse and _resets_in utility

The get_quota method handles timestamp eviction and quota counting. record_request calls it internally so both the GET /users/{id}/quota and POST /requests endpoints share the same logic without duplication.

_resets_in is extracted as a private utility function because both get_quota and record_request need to calculate time until reset. In record_request specifically, _resets_in is called again after appending the new timestamp. This is necessary to handle the case where the request log was empty before the request. Without recalculating, a user's first request would return resets_in_seconds = 0 since get_quota would have seen an empty log. After appending, _resets_in correctly returns ~60 seconds. The underscore prefix signals it is an internal implementation detail, not part of the public interface.

Trade-offs

In-memory storage

All data lives in Python dicts and deques, nothing persists if the server restarts. This is fine for the assignment scope as in-memory storage was explicitly specified.

No authentication

The POST /requests endpoint trusts the caller to send the correct user_id. In production you'd derive the user from an auth token or API key instead.

Thread safety

The race condition where two concurrent requests for the same user both pass the quota check can't happen here as we only run a single process. In a multi-process or multi-node deployment this would become a real problem, but at that point the in-memory store wouldn't be shared across processes anyway, which is the bigger issue. Both problems would need to be solved together by moving to a shared data store.

Integration tests over unit tests

I tested the API layer directly rather than writing separate unit tests for the Store class. The store logic (rolling window eviction, quota counting, and resets) is fully tested through the endpoint tests. Adding unit tests for the store would largely duplicate that coverage without adding value. At this scale, the integration tests give more confidence with less code.

Read side effects

GET /users/{id}/quota mutates state. It evicts expired timestamps from the request log during the quota check. Ideally reads have no side effects, but the eviction is necessary to return an accurate count. Alternative would be a background cleanup job for example.

No rate-limit headers

A production API could include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response. I kept it simple and only included a Retry-After header on 429 responses.

About

Build a small backend service that enforces request limits per user. Each user is allowed a maximum of 10 requests (POST /requests) per rolling 1-minute window.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Rate Limiter Service

A small FastAPI service that enforces per-user request rate limits using a rolling 1-minute window.

How to run

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
fastapi dev

API docs available at http://127.0.0.1:8000/docs

How to package

zip -r submission.zip . -x "*.pyc" -x "*/__pycache__/*" -x ".venv/*" -x ".pytest_cache/*" -x "*.pdf" -x ".git/*" -x "*.zip"

How to test

pytest -v

The test suite covers:

  • User creation and auto-incrementing IDs
  • Request recording, remaining decrement, and independent quotas per user
  • Rate limiting at the 10 request limit with Retry-After header validation
  • Input validation: missing body, invalid types, and non-existent users
  • Quota endpoint covering fresh users, partial usage, and exhausted quota
  • Window reset behaviour: rather than sleeping 60 seconds, the time module inside store.py is patched to return a future timestamp, verifying the rolling window resets correctly without slowing the test suite down by using something like time.sleep()

Design Decisions

Why FastAPI

The assignment gave the choice between Django and FastAPI. I went with FastAPI because it's lighter and faster to set up. I also noticed from the job description that Sendcloud uses FastAPI, so it felt like the right fit. FastAPI's integration with Pydantic gives input validation and auto-generated OpenAPI docs out of the box, with the interactive docs available at /docs without any extra configuration. If the API grew, I'd add summary, description, and tags to each endpoint to keep the docs readable and useful.

Rolling Window Algorithm

Each user has a deque of request timestamps. When checking quota, I remove any timestamps older than 60 seconds from the front, then count what's left. If there are fewer than 10, the request goes through and the new timestamp gets appended.

I used a deque instead of a regular list because removing from the front is O(1) with popleft() vs O(n) with list.pop(0). At max 10 entries this doesn't really matter performance-wise, but it's the right data structure for the job.

Keeping User as a plain dataclass

I considered moving the window eviction logic into the User class itself, which would be more of a DDD approach. I decided against it because I wanted User to stay a simple data container and keep the business logic in the Store. For a service this small, mixing data and behaviour in the same class felt like unnecessary complexity.

User lookup at the endpoint layer

I call get_user in the endpoint rather than inside the store methods like record_request or get_quota. This keeps the store focused purely on business logic. Its methods assume they receive a valid User and don't need to handle the not-found case. The endpoint layer is the right place for HTTP concerns like returning a 404. If user lookup were inside the store, it would need to decide what to return when the user doesn't exist, which leaks HTTP-level concerns into the data layer.

get_quota reuse and _resets_in utility

The get_quota method handles timestamp eviction and quota counting. record_request calls it internally so both the GET /users/{id}/quota and POST /requests endpoints share the same logic without duplication.

_resets_in is extracted as a private utility function because both get_quota and record_request need to calculate time until reset. In record_request specifically, _resets_in is called again after appending the new timestamp. This is necessary to handle the case where the request log was empty before the request. Without recalculating, a user's first request would return resets_in_seconds = 0 since get_quota would have seen an empty log. After appending, _resets_in correctly returns ~60 seconds. The underscore prefix signals it is an internal implementation detail, not part of the public interface.

Trade-offs

In-memory storage

All data lives in Python dicts and deques, nothing persists if the server restarts. This is fine for the assignment scope as in-memory storage was explicitly specified.

No authentication

The POST /requests endpoint trusts the caller to send the correct user_id. In production you'd derive the user from an auth token or API key instead.

Thread safety

The race condition where two concurrent requests for the same user both pass the quota check can't happen here as we only run a single process. In a multi-process or multi-node deployment this would become a real problem, but at that point the in-memory store wouldn't be shared across processes anyway, which is the bigger issue. Both problems would need to be solved together by moving to a shared data store.

Integration tests over unit tests

I tested the API layer directly rather than writing separate unit tests for the Store class. The store logic (rolling window eviction, quota counting, and resets) is fully tested through the endpoint tests. Adding unit tests for the store would largely duplicate that coverage without adding value. At this scale, the integration tests give more confidence with less code.

Read side effects

GET /users/{id}/quota mutates state. It evicts expired timestamps from the request log during the quota check. Ideally reads have no side effects, but the eviction is necessary to return an accurate count. Alternative would be a background cleanup job for example.

No rate-limit headers

A production API could include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response. I kept it simple and only included a Retry-After header on 429 responses.

About

Build a small backend service that enforces request limits per user. Each user is allowed a maximum of 10 requests (POST /requests) per rolling 1-minute window.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Rate Limiter Service

A small FastAPI service that enforces per-user request rate limits using a rolling 1-minute window.

How to run

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
fastapi dev

API docs available at http://127.0.0.1:8000/docs

How to package

zip -r submission.zip . -x "*.pyc" -x "*/__pycache__/*" -x ".venv/*" -x ".pytest_cache/*" -x "*.pdf" -x ".git/*" -x "*.zip"

How to test

pytest -v

The test suite covers:

  • User creation and auto-incrementing IDs
  • Request recording, remaining decrement, and independent quotas per user
  • Rate limiting at the 10 request limit with Retry-After header validation
  • Input validation: missing body, invalid types, and non-existent users
  • Quota endpoint covering fresh users, partial usage, and exhausted quota
  • Window reset behaviour: rather than sleeping 60 seconds, the time module inside store.py is patched to return a future timestamp, verifying the rolling window resets correctly without slowing the test suite down by using something like time.sleep()

Design Decisions

Why FastAPI

The assignment gave the choice between Django and FastAPI. I went with FastAPI because it's lighter and faster to set up. I also noticed from the job description that Sendcloud uses FastAPI, so it felt like the right fit. FastAPI's integration with Pydantic gives input validation and auto-generated OpenAPI docs out of the box, with the interactive docs available at /docs without any extra configuration. If the API grew, I'd add summary, description, and tags to each endpoint to keep the docs readable and useful.

Rolling Window Algorithm

Each user has a deque of request timestamps. When checking quota, I remove any timestamps older than 60 seconds from the front, then count what's left. If there are fewer than 10, the request goes through and the new timestamp gets appended.

I used a deque instead of a regular list because removing from the front is O(1) with popleft() vs O(n) with list.pop(0). At max 10 entries this doesn't really matter performance-wise, but it's the right data structure for the job.

Keeping User as a plain dataclass

I considered moving the window eviction logic into the User class itself, which would be more of a DDD approach. I decided against it because I wanted User to stay a simple data container and keep the business logic in the Store. For a service this small, mixing data and behaviour in the same class felt like unnecessary complexity.

User lookup at the endpoint layer

I call get_user in the endpoint rather than inside the store methods like record_request or get_quota. This keeps the store focused purely on business logic. Its methods assume they receive a valid User and don't need to handle the not-found case. The endpoint layer is the right place for HTTP concerns like returning a 404. If user lookup were inside the store, it would need to decide what to return when the user doesn't exist, which leaks HTTP-level concerns into the data layer.

get_quota reuse and _resets_in utility

The get_quota method handles timestamp eviction and quota counting. record_request calls it internally so both the GET /users/{id}/quota and POST /requests endpoints share the same logic without duplication.

_resets_in is extracted as a private utility function because both get_quota and record_request need to calculate time until reset. In record_request specifically, _resets_in is called again after appending the new timestamp. This is necessary to handle the case where the request log was empty before the request. Without recalculating, a user's first request would return resets_in_seconds = 0 since get_quota would have seen an empty log. After appending, _resets_in correctly returns ~60 seconds. The underscore prefix signals it is an internal implementation detail, not part of the public interface.

Trade-offs

In-memory storage

All data lives in Python dicts and deques, nothing persists if the server restarts. This is fine for the assignment scope as in-memory storage was explicitly specified.

No authentication

The POST /requests endpoint trusts the caller to send the correct user_id. In production you'd derive the user from an auth token or API key instead.

Thread safety

The race condition where two concurrent requests for the same user both pass the quota check can't happen here as we only run a single process. In a multi-process or multi-node deployment this would become a real problem, but at that point the in-memory store wouldn't be shared across processes anyway, which is the bigger issue. Both problems would need to be solved together by moving to a shared data store.

Integration tests over unit tests

I tested the API layer directly rather than writing separate unit tests for the Store class. The store logic (rolling window eviction, quota counting, and resets) is fully tested through the endpoint tests. Adding unit tests for the store would largely duplicate that coverage without adding value. At this scale, the integration tests give more confidence with less code.

Read side effects

GET /users/{id}/quota mutates state. It evicts expired timestamps from the request log during the quota check. Ideally reads have no side effects, but the eviction is necessary to return an accurate count. Alternative would be a background cleanup job for example.

No rate-limit headers

A production API could include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response. I kept it simple and only included a Retry-After header on 429 responses.

About

Build a small backend service that enforces request limits per user. Each user is allowed a maximum of 10 requests (POST /requests) per rolling 1-minute window.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Rate Limiter Service

A small FastAPI service that enforces per-user request rate limits using a rolling 1-minute window.

How to run

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
fastapi dev

API docs available at http://127.0.0.1:8000/docs

How to package

zip -r submission.zip . -x "*.pyc" -x "*/__pycache__/*" -x ".venv/*" -x ".pytest_cache/*" -x "*.pdf" -x ".git/*" -x "*.zip"

How to test

pytest -v

The test suite covers:

  • User creation and auto-incrementing IDs
  • Request recording, remaining decrement, and independent quotas per user
  • Rate limiting at the 10 request limit with Retry-After header validation
  • Input validation: missing body, invalid types, and non-existent users
  • Quota endpoint covering fresh users, partial usage, and exhausted quota
  • Window reset behaviour: rather than sleeping 60 seconds, the time module inside store.py is patched to return a future timestamp, verifying the rolling window resets correctly without slowing the test suite down by using something like time.sleep()

Design Decisions

Why FastAPI

The assignment gave the choice between Django and FastAPI. I went with FastAPI because it's lighter and faster to set up. I also noticed from the job description that Sendcloud uses FastAPI, so it felt like the right fit. FastAPI's integration with Pydantic gives input validation and auto-generated OpenAPI docs out of the box, with the interactive docs available at /docs without any extra configuration. If the API grew, I'd add summary, description, and tags to each endpoint to keep the docs readable and useful.

Rolling Window Algorithm

Each user has a deque of request timestamps. When checking quota, I remove any timestamps older than 60 seconds from the front, then count what's left. If there are fewer than 10, the request goes through and the new timestamp gets appended.

I used a deque instead of a regular list because removing from the front is O(1) with popleft() vs O(n) with list.pop(0). At max 10 entries this doesn't really matter performance-wise, but it's the right data structure for the job.

Keeping User as a plain dataclass

I considered moving the window eviction logic into the User class itself, which would be more of a DDD approach. I decided against it because I wanted User to stay a simple data container and keep the business logic in the Store. For a service this small, mixing data and behaviour in the same class felt like unnecessary complexity.

User lookup at the endpoint layer

I call get_user in the endpoint rather than inside the store methods like record_request or get_quota. This keeps the store focused purely on business logic. Its methods assume they receive a valid User and don't need to handle the not-found case. The endpoint layer is the right place for HTTP concerns like returning a 404. If user lookup were inside the store, it would need to decide what to return when the user doesn't exist, which leaks HTTP-level concerns into the data layer.

get_quota reuse and _resets_in utility

The get_quota method handles timestamp eviction and quota counting. record_request calls it internally so both the GET /users/{id}/quota and POST /requests endpoints share the same logic without duplication.

_resets_in is extracted as a private utility function because both get_quota and record_request need to calculate time until reset. In record_request specifically, _resets_in is called again after appending the new timestamp. This is necessary to handle the case where the request log was empty before the request. Without recalculating, a user's first request would return resets_in_seconds = 0 since get_quota would have seen an empty log. After appending, _resets_in correctly returns ~60 seconds. The underscore prefix signals it is an internal implementation detail, not part of the public interface.

Trade-offs

In-memory storage

All data lives in Python dicts and deques, nothing persists if the server restarts. This is fine for the assignment scope as in-memory storage was explicitly specified.

No authentication

The POST /requests endpoint trusts the caller to send the correct user_id. In production you'd derive the user from an auth token or API key instead.

Thread safety

The race condition where two concurrent requests for the same user both pass the quota check can't happen here as we only run a single process. In a multi-process or multi-node deployment this would become a real problem, but at that point the in-memory store wouldn't be shared across processes anyway, which is the bigger issue. Both problems would need to be solved together by moving to a shared data store.

Integration tests over unit tests

I tested the API layer directly rather than writing separate unit tests for the Store class. The store logic (rolling window eviction, quota counting, and resets) is fully tested through the endpoint tests. Adding unit tests for the store would largely duplicate that coverage without adding value. At this scale, the integration tests give more confidence with less code.

Read side effects

GET /users/{id}/quota mutates state. It evicts expired timestamps from the request log during the quota check. Ideally reads have no side effects, but the eviction is necessary to return an accurate count. Alternative would be a background cleanup job for example.

No rate-limit headers

A production API could include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response. I kept it simple and only included a Retry-After header on 429 responses.

About

Build a small backend service that enforces request limits per user. Each user is allowed a maximum of 10 requests (POST /requests) per rolling 1-minute window.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Rate Limiter Service

A small FastAPI service that enforces per-user request rate limits using a rolling 1-minute window.

How to run

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
fastapi dev

API docs available at http://127.0.0.1:8000/docs

How to package

zip -r submission.zip . -x "*.pyc" -x "*/__pycache__/*" -x ".venv/*" -x ".pytest_cache/*" -x "*.pdf" -x ".git/*" -x "*.zip"

How to test

pytest -v

The test suite covers:

  • User creation and auto-incrementing IDs
  • Request recording, remaining decrement, and independent quotas per user
  • Rate limiting at the 10 request limit with Retry-After header validation
  • Input validation: missing body, invalid types, and non-existent users
  • Quota endpoint covering fresh users, partial usage, and exhausted quota
  • Window reset behaviour: rather than sleeping 60 seconds, the time module inside store.py is patched to return a future timestamp, verifying the rolling window resets correctly without slowing the test suite down by using something like time.sleep()

Design Decisions

Why FastAPI

The assignment gave the choice between Django and FastAPI. I went with FastAPI because it's lighter and faster to set up. I also noticed from the job description that Sendcloud uses FastAPI, so it felt like the right fit. FastAPI's integration with Pydantic gives input validation and auto-generated OpenAPI docs out of the box, with the interactive docs available at /docs without any extra configuration. If the API grew, I'd add summary, description, and tags to each endpoint to keep the docs readable and useful.

Rolling Window Algorithm

Each user has a deque of request timestamps. When checking quota, I remove any timestamps older than 60 seconds from the front, then count what's left. If there are fewer than 10, the request goes through and the new timestamp gets appended.

I used a deque instead of a regular list because removing from the front is O(1) with popleft() vs O(n) with list.pop(0). At max 10 entries this doesn't really matter performance-wise, but it's the right data structure for the job.

Keeping User as a plain dataclass

I considered moving the window eviction logic into the User class itself, which would be more of a DDD approach. I decided against it because I wanted User to stay a simple data container and keep the business logic in the Store. For a service this small, mixing data and behaviour in the same class felt like unnecessary complexity.

User lookup at the endpoint layer

I call get_user in the endpoint rather than inside the store methods like record_request or get_quota. This keeps the store focused purely on business logic. Its methods assume they receive a valid User and don't need to handle the not-found case. The endpoint layer is the right place for HTTP concerns like returning a 404. If user lookup were inside the store, it would need to decide what to return when the user doesn't exist, which leaks HTTP-level concerns into the data layer.

get_quota reuse and _resets_in utility

The get_quota method handles timestamp eviction and quota counting. record_request calls it internally so both the GET /users/{id}/quota and POST /requests endpoints share the same logic without duplication.

_resets_in is extracted as a private utility function because both get_quota and record_request need to calculate time until reset. In record_request specifically, _resets_in is called again after appending the new timestamp. This is necessary to handle the case where the request log was empty before the request. Without recalculating, a user's first request would return resets_in_seconds = 0 since get_quota would have seen an empty log. After appending, _resets_in correctly returns ~60 seconds. The underscore prefix signals it is an internal implementation detail, not part of the public interface.

Trade-offs

In-memory storage

All data lives in Python dicts and deques, nothing persists if the server restarts. This is fine for the assignment scope as in-memory storage was explicitly specified.

No authentication

The POST /requests endpoint trusts the caller to send the correct user_id. In production you'd derive the user from an auth token or API key instead.

Thread safety

The race condition where two concurrent requests for the same user both pass the quota check can't happen here as we only run a single process. In a multi-process or multi-node deployment this would become a real problem, but at that point the in-memory store wouldn't be shared across processes anyway, which is the bigger issue. Both problems would need to be solved together by moving to a shared data store.

Integration tests over unit tests

I tested the API layer directly rather than writing separate unit tests for the Store class. The store logic (rolling window eviction, quota counting, and resets) is fully tested through the endpoint tests. Adding unit tests for the store would largely duplicate that coverage without adding value. At this scale, the integration tests give more confidence with less code.

Read side effects

GET /users/{id}/quota mutates state. It evicts expired timestamps from the request log during the quota check. Ideally reads have no side effects, but the eviction is necessary to return an accurate count. Alternative would be a background cleanup job for example.

No rate-limit headers

A production API could include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response. I kept it simple and only included a Retry-After header on 429 responses.

About

Build a small backend service that enforces request limits per user. Each user is allowed a maximum of 10 requests (POST /requests) per rolling 1-minute window.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Rate Limiter Service

A small FastAPI service that enforces per-user request rate limits using a rolling 1-minute window.

How to run

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
fastapi dev

API docs available at http://127.0.0.1:8000/docs

How to package

zip -r submission.zip . -x "*.pyc" -x "*/__pycache__/*" -x ".venv/*" -x ".pytest_cache/*" -x "*.pdf" -x ".git/*" -x "*.zip"

How to test

pytest -v

The test suite covers:

  • User creation and auto-incrementing IDs
  • Request recording, remaining decrement, and independent quotas per user
  • Rate limiting at the 10 request limit with Retry-After header validation
  • Input validation: missing body, invalid types, and non-existent users
  • Quota endpoint covering fresh users, partial usage, and exhausted quota
  • Window reset behaviour: rather than sleeping 60 seconds, the time module inside store.py is patched to return a future timestamp, verifying the rolling window resets correctly without slowing the test suite down by using something like time.sleep()

Design Decisions

Why FastAPI

The assignment gave the choice between Django and FastAPI. I went with FastAPI because it's lighter and faster to set up. I also noticed from the job description that Sendcloud uses FastAPI, so it felt like the right fit. FastAPI's integration with Pydantic gives input validation and auto-generated OpenAPI docs out of the box, with the interactive docs available at /docs without any extra configuration. If the API grew, I'd add summary, description, and tags to each endpoint to keep the docs readable and useful.

Rolling Window Algorithm

Each user has a deque of request timestamps. When checking quota, I remove any timestamps older than 60 seconds from the front, then count what's left. If there are fewer than 10, the request goes through and the new timestamp gets appended.

I used a deque instead of a regular list because removing from the front is O(1) with popleft() vs O(n) with list.pop(0). At max 10 entries this doesn't really matter performance-wise, but it's the right data structure for the job.

Keeping User as a plain dataclass

I considered moving the window eviction logic into the User class itself, which would be more of a DDD approach. I decided against it because I wanted User to stay a simple data container and keep the business logic in the Store. For a service this small, mixing data and behaviour in the same class felt like unnecessary complexity.

User lookup at the endpoint layer

I call get_user in the endpoint rather than inside the store methods like record_request or get_quota. This keeps the store focused purely on business logic. Its methods assume they receive a valid User and don't need to handle the not-found case. The endpoint layer is the right place for HTTP concerns like returning a 404. If user lookup were inside the store, it would need to decide what to return when the user doesn't exist, which leaks HTTP-level concerns into the data layer.

get_quota reuse and _resets_in utility

The get_quota method handles timestamp eviction and quota counting. record_request calls it internally so both the GET /users/{id}/quota and POST /requests endpoints share the same logic without duplication.

_resets_in is extracted as a private utility function because both get_quota and record_request need to calculate time until reset. In record_request specifically, _resets_in is called again after appending the new timestamp. This is necessary to handle the case where the request log was empty before the request. Without recalculating, a user's first request would return resets_in_seconds = 0 since get_quota would have seen an empty log. After appending, _resets_in correctly returns ~60 seconds. The underscore prefix signals it is an internal implementation detail, not part of the public interface.

Trade-offs

In-memory storage

All data lives in Python dicts and deques, nothing persists if the server restarts. This is fine for the assignment scope as in-memory storage was explicitly specified.

No authentication

The POST /requests endpoint trusts the caller to send the correct user_id. In production you'd derive the user from an auth token or API key instead.

Thread safety

The race condition where two concurrent requests for the same user both pass the quota check can't happen here as we only run a single process. In a multi-process or multi-node deployment this would become a real problem, but at that point the in-memory store wouldn't be shared across processes anyway, which is the bigger issue. Both problems would need to be solved together by moving to a shared data store.

Integration tests over unit tests

I tested the API layer directly rather than writing separate unit tests for the Store class. The store logic (rolling window eviction, quota counting, and resets) is fully tested through the endpoint tests. Adding unit tests for the store would largely duplicate that coverage without adding value. At this scale, the integration tests give more confidence with less code.

Read side effects

GET /users/{id}/quota mutates state. It evicts expired timestamps from the request log during the quota check. Ideally reads have no side effects, but the eviction is necessary to return an accurate count. Alternative would be a background cleanup job for example.

No rate-limit headers

A production API could include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response. I kept it simple and only included a Retry-After header on 429 responses.

About

Build a small backend service that enforces request limits per user. Each user is allowed a maximum of 10 requests (POST /requests) per rolling 1-minute window.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Rate Limiter Service

A small FastAPI service that enforces per-user request rate limits using a rolling 1-minute window.

How to run

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
fastapi dev

API docs available at http://127.0.0.1:8000/docs

How to package

zip -r submission.zip . -x "*.pyc" -x "*/__pycache__/*" -x ".venv/*" -x ".pytest_cache/*" -x "*.pdf" -x ".git/*" -x "*.zip"

How to test

pytest -v

The test suite covers:

  • User creation and auto-incrementing IDs
  • Request recording, remaining decrement, and independent quotas per user
  • Rate limiting at the 10 request limit with Retry-After header validation
  • Input validation: missing body, invalid types, and non-existent users
  • Quota endpoint covering fresh users, partial usage, and exhausted quota
  • Window reset behaviour: rather than sleeping 60 seconds, the time module inside store.py is patched to return a future timestamp, verifying the rolling window resets correctly without slowing the test suite down by using something like time.sleep()

Design Decisions

Why FastAPI

The assignment gave the choice between Django and FastAPI. I went with FastAPI because it's lighter and faster to set up. I also noticed from the job description that Sendcloud uses FastAPI, so it felt like the right fit. FastAPI's integration with Pydantic gives input validation and auto-generated OpenAPI docs out of the box, with the interactive docs available at /docs without any extra configuration. If the API grew, I'd add summary, description, and tags to each endpoint to keep the docs readable and useful.

Rolling Window Algorithm

Each user has a deque of request timestamps. When checking quota, I remove any timestamps older than 60 seconds from the front, then count what's left. If there are fewer than 10, the request goes through and the new timestamp gets appended.

I used a deque instead of a regular list because removing from the front is O(1) with popleft() vs O(n) with list.pop(0). At max 10 entries this doesn't really matter performance-wise, but it's the right data structure for the job.

Keeping User as a plain dataclass

I considered moving the window eviction logic into the User class itself, which would be more of a DDD approach. I decided against it because I wanted User to stay a simple data container and keep the business logic in the Store. For a service this small, mixing data and behaviour in the same class felt like unnecessary complexity.

User lookup at the endpoint layer

I call get_user in the endpoint rather than inside the store methods like record_request or get_quota. This keeps the store focused purely on business logic. Its methods assume they receive a valid User and don't need to handle the not-found case. The endpoint layer is the right place for HTTP concerns like returning a 404. If user lookup were inside the store, it would need to decide what to return when the user doesn't exist, which leaks HTTP-level concerns into the data layer.

get_quota reuse and _resets_in utility

The get_quota method handles timestamp eviction and quota counting. record_request calls it internally so both the GET /users/{id}/quota and POST /requests endpoints share the same logic without duplication.

_resets_in is extracted as a private utility function because both get_quota and record_request need to calculate time until reset. In record_request specifically, _resets_in is called again after appending the new timestamp. This is necessary to handle the case where the request log was empty before the request. Without recalculating, a user's first request would return resets_in_seconds = 0 since get_quota would have seen an empty log. After appending, _resets_in correctly returns ~60 seconds. The underscore prefix signals it is an internal implementation detail, not part of the public interface.

Trade-offs

In-memory storage

All data lives in Python dicts and deques, nothing persists if the server restarts. This is fine for the assignment scope as in-memory storage was explicitly specified.

No authentication

The POST /requests endpoint trusts the caller to send the correct user_id. In production you'd derive the user from an auth token or API key instead.

Thread safety

The race condition where two concurrent requests for the same user both pass the quota check can't happen here as we only run a single process. In a multi-process or multi-node deployment this would become a real problem, but at that point the in-memory store wouldn't be shared across processes anyway, which is the bigger issue. Both problems would need to be solved together by moving to a shared data store.

Integration tests over unit tests

I tested the API layer directly rather than writing separate unit tests for the Store class. The store logic (rolling window eviction, quota counting, and resets) is fully tested through the endpoint tests. Adding unit tests for the store would largely duplicate that coverage without adding value. At this scale, the integration tests give more confidence with less code.

Read side effects

GET /users/{id}/quota mutates state. It evicts expired timestamps from the request log during the quota check. Ideally reads have no side effects, but the eviction is necessary to return an accurate count. Alternative would be a background cleanup job for example.

No rate-limit headers

A production API could include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response. I kept it simple and only included a Retry-After header on 429 responses.

About

Build a small backend service that enforces request limits per user. Each user is allowed a maximum of 10 requests (POST /requests) per rolling 1-minute window.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Rate Limiter Service

A small FastAPI service that enforces per-user request rate limits using a rolling 1-minute window.

How to run

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
fastapi dev

API docs available at http://127.0.0.1:8000/docs

How to package

zip -r submission.zip . -x "*.pyc" -x "*/__pycache__/*" -x ".venv/*" -x ".pytest_cache/*" -x "*.pdf" -x ".git/*" -x "*.zip"

How to test

pytest -v

The test suite covers:

  • User creation and auto-incrementing IDs
  • Request recording, remaining decrement, and independent quotas per user
  • Rate limiting at the 10 request limit with Retry-After header validation
  • Input validation: missing body, invalid types, and non-existent users
  • Quota endpoint covering fresh users, partial usage, and exhausted quota
  • Window reset behaviour: rather than sleeping 60 seconds, the time module inside store.py is patched to return a future timestamp, verifying the rolling window resets correctly without slowing the test suite down by using something like time.sleep()

Design Decisions

Why FastAPI

The assignment gave the choice between Django and FastAPI. I went with FastAPI because it's lighter and faster to set up. I also noticed from the job description that Sendcloud uses FastAPI, so it felt like the right fit. FastAPI's integration with Pydantic gives input validation and auto-generated OpenAPI docs out of the box, with the interactive docs available at /docs without any extra configuration. If the API grew, I'd add summary, description, and tags to each endpoint to keep the docs readable and useful.

Rolling Window Algorithm

Each user has a deque of request timestamps. When checking quota, I remove any timestamps older than 60 seconds from the front, then count what's left. If there are fewer than 10, the request goes through and the new timestamp gets appended.

I used a deque instead of a regular list because removing from the front is O(1) with popleft() vs O(n) with list.pop(0). At max 10 entries this doesn't really matter performance-wise, but it's the right data structure for the job.

Keeping User as a plain dataclass

I considered moving the window eviction logic into the User class itself, which would be more of a DDD approach. I decided against it because I wanted User to stay a simple data container and keep the business logic in the Store. For a service this small, mixing data and behaviour in the same class felt like unnecessary complexity.

User lookup at the endpoint layer

I call get_user in the endpoint rather than inside the store methods like record_request or get_quota. This keeps the store focused purely on business logic. Its methods assume they receive a valid User and don't need to handle the not-found case. The endpoint layer is the right place for HTTP concerns like returning a 404. If user lookup were inside the store, it would need to decide what to return when the user doesn't exist, which leaks HTTP-level concerns into the data layer.

get_quota reuse and _resets_in utility

The get_quota method handles timestamp eviction and quota counting. record_request calls it internally so both the GET /users/{id}/quota and POST /requests endpoints share the same logic without duplication.

_resets_in is extracted as a private utility function because both get_quota and record_request need to calculate time until reset. In record_request specifically, _resets_in is called again after appending the new timestamp. This is necessary to handle the case where the request log was empty before the request. Without recalculating, a user's first request would return resets_in_seconds = 0 since get_quota would have seen an empty log. After appending, _resets_in correctly returns ~60 seconds. The underscore prefix signals it is an internal implementation detail, not part of the public interface.

Trade-offs

In-memory storage

All data lives in Python dicts and deques, nothing persists if the server restarts. This is fine for the assignment scope as in-memory storage was explicitly specified.

No authentication

The POST /requests endpoint trusts the caller to send the correct user_id. In production you'd derive the user from an auth token or API key instead.

Thread safety

The race condition where two concurrent requests for the same user both pass the quota check can't happen here as we only run a single process. In a multi-process or multi-node deployment this would become a real problem, but at that point the in-memory store wouldn't be shared across processes anyway, which is the bigger issue. Both problems would need to be solved together by moving to a shared data store.

Integration tests over unit tests

I tested the API layer directly rather than writing separate unit tests for the Store class. The store logic (rolling window eviction, quota counting, and resets) is fully tested through the endpoint tests. Adding unit tests for the store would largely duplicate that coverage without adding value. At this scale, the integration tests give more confidence with less code.

Read side effects

GET /users/{id}/quota mutates state. It evicts expired timestamps from the request log during the quota check. Ideally reads have no side effects, but the eviction is necessary to return an accurate count. Alternative would be a background cleanup job for example.

No rate-limit headers

A production API could include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response. I kept it simple and only included a Retry-After header on 429 responses.

About

Build a small backend service that enforces request limits per user. Each user is allowed a maximum of 10 requests (POST /requests) per rolling 1-minute window.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages