Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added gateway/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions gateway/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
# Services available from Module 3
user_service_url: str = "http://localhost:8001"
game_service_url: str = "http://localhost:8002"
activity_service_url: str = "http://localhost:8003"

# Added in Module 4
# notification_service_url: str = "http://localhost:8004"

# Added in Module 5
# logging_service_url: str = "http://localhost:8006"

# Added in Module 6
# auth_service_url: str = "http://localhost:8005"
# secret_key: str = "dev-secret-change-in-production"

class Config:
env_file = ".env"


settings = Settings()
103 changes: 103 additions & 0 deletions gateway/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import httpx
from fastapi import FastAPI, Request, Response

from app.config import settings

app = FastAPI(title="gateway", version="1.0.0")

# Routing table — maps the resource name in the URL path to the target service base URL.
# Path structure: /{version}/{resource}/...
# e.g. GET /v1/users/123 → resource = "users" → forward to user_service_url
#
# Add new entries here as each module introduces a new service.
# Module 4 will add: "notifications"
# Module 5 will add: "consent", "logs"
# Module 6 will add: "auth"
ROUTES: dict[str, str] = {
"users": settings.user_service_url,
"games": settings.game_service_url,
"activities": settings.activity_service_url,
}


@app.get("/health")
async def health():
"""
Gateway liveness check. Handled here — never forwarded to a service.
In Module 10 this endpoint will be upgraded to fan out to all services
and return their individual status.
"""
return {"status": "ok", "service": "gateway"}


@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
async def proxy(request: Request, path: str):
"""
Catch-all reverse proxy — forwards every request to the correct downstream service.

Your job: implement the forwarding logic described below.

---
Step 1 — Parse the path to find the resource name.

The path arrives without the leading slash, e.g. "v1/users/123".
Split it on "/" to get a list of segments:
segments = path.split("/")
# ["v1", "users", "123"]

Index 0 is the version ("v1").
Index 1 is the resource ("users", "games", "activities", ...).

If the path has fewer than 2 segments, return a 404.

---
Step 2 — Look up the resource in ROUTES.

resource = segments[1]
target_base = ROUTES.get(resource)

If `resource` is not in ROUTES, return:
Response(status_code=404, content=f"Unknown resource: {resource}")

---
Step 3 — Build the target URL and forward the request.

The full path must be forwarded as-is — no stripping, no rewriting.
Reconstruct it with the leading slash:
target_url = f"{target_base}/{path}"

Forward using httpx, preserving method, headers, and body:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.request(
method=request.method,
url=target_url,
headers=request.headers.raw, # forward all original headers
content=await request.body(), # forward the body as-is
params=request.query_params, # forward query string
)

Return a FastAPI Response with the downstream status, headers, and body:
return Response(
content=response.content,
status_code=response.status_code,
headers=dict(response.headers),
media_type=response.headers.get("content-type"),
)

---
Step 4 — Handle an unreachable downstream service.

Wrap the httpx call in a try/except for httpx.RequestError.
If the service cannot be reached, return:
Response(status_code=503, content="Service unavailable")

---
Verify your implementation:
curl http://localhost:8000/health
curl http://localhost:8000/v1/users
curl http://localhost:8000/v1/games
curl http://localhost:8000/v1/activities
curl http://localhost:8000/v1/unknown # should return 404
"""
# TODO: implement steps 1–4 above
raise NotImplementedError("implement the proxy forwarding logic")
4 changes: 4 additions & 0 deletions gateway/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fastapi==0.115.0
uvicorn[standard]==0.30.6
httpx==0.27.2
pydantic-settings==2.4.0
6 changes: 3 additions & 3 deletions modules/module-01/REFLECTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ You started from a painful monolith. Now you're splitting it into separate servi

Think about it from three angles: the developer who has to change code, the team that has to deploy it, and the user who has to live with its failures. You don't need to cover all three, pick the one that felt most real to you today.

> _Your answer:_
> _Your answer:_ so basically the problem we had with the monolith is if one thing breaks everything breaks, like if you just wanna fix the genre of a game in the catalog you gotta restart the whole app and while its restarting users cant log in, activities stop, notifications stop, like everything goes down for one tiny change that had nothing to do with any of that. splitting it into services means when the game-service restarts, the user-service doesnt even notice, users can still log in fine. so for the developer its easier to change one thing without being scared it breaks something else, and for the user they dont get kicked out just because someone fixed a typo somewhere.

---

Expand All @@ -34,7 +34,7 @@ Look at your service map. Every arrow between two services is a decision someone

What would break, slow down, or become harder to manage if you merged those two services back together?

> _Your answer:_
> _Your answer:_the line i think is clearest is between user-service and activity-service. user-service owns users, activity-service owns activities. if we merged them together then every time we want to change something about how activities work, we'd also be touching the user code, and one bug could mess up logins. also they scale differently — if the platform gets busy with activity tracking you want to scale just that part up, not the whole user management system. keeping them separate means each one can grow at its own speed without dragging the other one.

---

Expand All @@ -46,7 +46,7 @@ Microservices solve the monolith's problems. But they create new ones.

No need to solve it: just name it honestly. This is exactly the tension the rest of the course is about.

> _Your answer:_
> _Your answer:_in the monolith, if nova logs an activity and you want to check her username, you just do a JOIN — one database query, done. now with microservices, activity-service doesnt have direct access to the users table, so it has to make a network call to user-service to get the username. that adds latency, and if user-service is down for any reason, the activity request could fail too. so we traded "easy data access" for "independent deployments" which is a good trade overall but its definitely more complicated to deal with.

---

Expand Down
39 changes: 39 additions & 0 deletions modules/module-01/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ For each bounded context you identify, fill in the table:
There is no single correct answer: what matters is that you can justify each row.

---
| Bounded Context | Responsibilities | Owned Entities | Team |
|-----------------|-----------------------------------------------------------|-----------------------------|--------------|
| Identity | it manages who users are and handles registration and profiles | User, Session | Platform |
| Game Library | it manages the game catalog, search and metadata | Game | Catalog |
| Activity | it tracks what users are playing and also logs game actions | Activity | Engagement |
| Notifications | it sends alerts to friends when someone logs an activity | Notification | Engagement |
| Logging | Records user actions for analytics and also to respect GDPR principles | ConsentRecord, ActivityLog | Platform |
| Auth | it issues and validates JWT tokens for all services | Token, Credential | Platform |

## Task 2 — Define service contracts _(~30 min)_

Expand All @@ -55,9 +63,37 @@ Payload: { activity_id, user_id, action, game_id, timestamp }
```

Focus on the flows that feel non-obvious. You do not need to document every possible pair.
**user-service → auth-service**
- Direction: user-service to auth-service
- Trigger: user logs in and needs a JWT token issued
- Protocol: REST (sync — the login response has to include the token)
- Payload: `{ user_id, username, email }`

---

**activity-service → logging-service**
- Direction: activity-service to logging-service
- Trigger: a user logs a new activity (started/completed a game)
- Protocol: RabbitMQ message (async — logging doesnt need to block the activity response, if logging is slow or down the activity still saves fine)
- Payload: `{ activity_id, user_id, game_id, action, timestamp }`

---

**activity-service → notification-service**
- Direction: activity-service to notification-service
- Trigger: a user logs an activity that their friends should see
- Protocol: RabbitMQ message (async — same reason as above, notifications are best-effort, they shouldnt slow down activity logging)
- Payload: `{ activity_id, user_id, game_id, action, timestamp }`

---

**gateway → all services**
- Direction: gateway to user-service / game-service / activity-service / etc
- Trigger: any client request coming in from the frontend
- Protocol: REST (sync — the client is waiting for a response)
- Payload: varies by route, gateway forwards the request with the JWT validated
---

## Task 3 — Draw the service map _(~20 min)_

Draw the full GameHub service map:
Expand All @@ -76,8 +112,11 @@ This can be a sketch on paper, a whiteboard photo, or ASCII art committed to you
Three questions to discuss as a team before you leave:

1. Why does `notification-service` use Node.js instead of Python like the rest? What does that tell you about microservices and technology choices?
its because in microservices each service can use whatever tech fits it best — notifications are event-driven and Node.js handles async event-based stuff really well. also it shows that microservices dont force you into one language, each team picks what makes sense for their problem.
2. What is the risk of `activity-service` calling `logging-service` synchronously — why might you prefer an async event instead?
if activity-service called logging-service synchronously and logging was slow or crashed, every activity request would also fail or be slow. with async (RabbitMQ), activity-service puts a message in the queue and moves on, logging picks it up whenever it can. the user gets a fast response and logging eventually catches up.
3. Why does `logging-service` need a GDPR consent check before recording any activity?
because recording what a user does is personal data. if the user said they dont want to be tracked, the logging-service has to respect that before writing anything. the consent has to be checked at the point of logging, not at signup, because a user can change their preference at any time.

You do not need to write these answers down — they are warm-up for your REFLECTION.md.

Expand Down
6 changes: 3 additions & 3 deletions modules/module-02/REFLECTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ You built a service with distinct layers: models, schemas, repository, service,

Think about what happens six months later when someone new joins the team, or when you need to swap SQLite for PostgreSQL. What does the layered structure protect you from?

> *Your answer:*
> *Your answer:*if you put everything in one file it works fine at first but 6 months later when someone new joins they open app.py and see SQL queries mixed with business logic mixed with HTTP responses and they have no idea where to even start looking. the layers help because if you need to swap SQLite for PostgreSQL, you only touch database.py and repository.py — routes.py and service.py dont need to change at all because they never talk to the database directly. same thing if the API shape changes, you only update schemas.py and routes.py and the rest of the code doesnt care. basically each layer protects you from having to change everything when one thing changes.

---

Expand All @@ -30,7 +30,7 @@ Each service owns its data exclusively — no other service is allowed to touch

Give a concrete scenario, not a general principle.

> *Your answer:*
> *Your answer:*game-service owns the games table. if activity-service could write to it directly, imagine this: activity-service has a bug and starts setting cover_url to null for every game it touches. now the games catalog is corrupted and game-service has no idea why because the writes are coming from somewhere else. it would be really hard to debug and game-service cant enforce its own rules about what valid game data looks like. when only game-service writes to its own table, if data is wrong you know exactly where to look.

---

Expand All @@ -42,7 +42,7 @@ You now have models, schemas, a repository, a service, and routes — five layer

And at what point does the complexity start to pay off? Where is the tipping point?

> *Your answer:*
> *Your answer:*for something this small with like 4 endpoints, having 5 separate files (models, schemas, repository, service, routes) feels like a lot of work for not much. in the monolith version of this we just wrote it all in app.py and it was done in like 30 minutes. the complexity starts paying off when the service grows — like when you add authentication, caching, or more complex business rules, having clean layers means you add that stuff in the right place without breaking what already works. for a tiny CRUD service the structure feels heavy, but the moment it needs to do more than basic CRUD it starts making sense.

---

Expand Down
5 changes: 3 additions & 2 deletions modules/module-03/REFLECTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ All client requests now go through the gateway. No client ever calls a service d
Think about what the client would need to know and manage if it talked to each service on its own port.

> *Your answer:*
honestly the thing that surprised me the most was how fragile it feels when one service needs to call another one. like when i was testing activity-service and forgot to start user-service first, everything just crashed. it made me realize that in a microservices setup you always have to think about what happens when the thing you depend on isnt there, which is something you never had to think about in the monolith because everything was in the same process.

---

Expand All @@ -31,7 +32,7 @@ The activity-service makes two outbound calls: one to validate the user (with re
What is the consequence for the user in each case if the downstream service is unavailable?

> *Your answer:*

the user validation has to block the request because theres no point saving an activity for a user that doesnt exist, that would just be bad data. but the game enrichment is just extra info we attach to make the response nicer,the activity itself is still valid without it. so if game-service is down we just return null for the game field and the activity still gets saved. treating them differently means a game-service outage doesnt stop users from logging activities.
---

## 3. The tradeoff
Expand All @@ -43,7 +44,7 @@ Every time a client creates an activity, three services are involved synchronous
What happens to the user experience if the slowest service in the chain takes 3 seconds to respond?

> *Your answer:*

every request now has to make an extra network hop through the gateway before it gets to the actual service. so if the gateway is slow or down, everything is slow or down even if all the services are fine. in the monolith there was no extra hop. but the trade-off is worth it because now clients only need to know one address and the gateway handles figuring out where to send things, and later it can also do auth checks in one place instead of every service doing it separately.
---

*Keep this file. You will refer back to it during the oral presentation.*
49 changes: 49 additions & 0 deletions modules/module-04/REFLECTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Module 4 — Reflection

**Team name**: ____fred___________
**Branch**: `module-04/<team-name>`
**Submitted**: before Module 5 lesson

---

Answer the three questions below. There are no right or wrong answers — we are looking for your reasoning, not a textbook definition. A few honest sentences are worth more than a long generic paragraph.

---

## 1. The "why"

In Module 3, services called each other directly over HTTP. Now activity-service drops a message into a broker and moves on — it never waits for a reply.

**What does the activity-service gain by not waiting? And what does the notification-service gain by consuming at its own pace?**

Think about what happens under load, or when notification-service is temporarily down.

> *Your answer:*

---so basically activity-service before had to wait for everything to finish before it could send back a response to the user, now it just drops the message in the queue and immediately says "ok done" without waiting for anyone to pick it up. this means if notification-service is super slow or even completely down, the user still gets a fast response and the activity still gets saved. notification-service on the other side can just process messages whenever it wants, if it gets 1000 notifications at once it can go through them one by one at its own speed instead of crashing because too many requests came in at the same time.

## 2. Your choice

In Module 3 you already knew how to call another service directly over HTTP — you did it for user validation and game enrichment.

**Why not use the same approach for notifications? What does introducing a broker give you that a direct HTTP call doesn't?**

Think about what happens if notification-service is slow, or crashes mid-message.

> *Your answer:*

---if we called notification-service directly over HTTP like we did for user validation, then if notification-service crashes in the middle of sending, the whole request fails or hangs. also if its slow, the user has to wait longer just because of notifications which has nothing to do with saving the activity. the broker fixes this because the message sits in the queue safely even if notification-service is down, when it comes back up it just picks up where it left off. with direct HTTP if the service is down the message is just gone.

## 3. The tradeoff

With synchronous REST, you get an immediate answer: success or failure. With async messaging, the activity is saved and the message is sent — but you have no idea if the notification was ever delivered.

**How would a user know if their notification was never sent? How would you know as a developer?**

What visibility do you lose when you go async?

> *Your answer:*

---yeah this is the annoying part of async, you basically have no idea if the notification actually got delivered or not. from the user side they might just never see the notification and have no idea why. from the developer side you would have to check the RabbitMQ UI manually or look at logs in notification-service to see if messages are being consumed. you lose the immediate feedback you get with REST where you know right away if it worked or failed. to fix this properly you would need to add some kind of logging or monitoring that tracks when a message was picked up and processed, but that adds more complexity.

*Keep this file. You will refer back to it during the oral presentation.*
9 changes: 9 additions & 0 deletions modules/module-04/docker-compose.override.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Module 4: Messaging infrastructure only (RabbitMQ)
# Services still run locally with uvicorn + SQLite.
# RabbitMQ is provided by docker-compose.infra.yml.
#
# Start with:
# docker compose -f docker-compose.infra.yml up -d rabbitmq
#
# No service containers in this module.
version: "3.9"
Loading