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
59 changes: 59 additions & 0 deletions gateway/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, Response
import httpx

app = FastAPI()

# Route table — add new services here
ROUTES = {
"users": "http://localhost:8001",
"games": "http://localhost:8002",
"activities": "http://localhost:8003",
}


@app.get("/health")
def health():
return {"status": "ok"}


@app.api_route("/v1/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def proxy(path: str, request: Request):

# Step 1 — extract the resource name
resource = path.split("/")[0]

# Step 2 — look up route
base_url = ROUTES.get(resource)

if not base_url:
return JSONResponse(
status_code=404, content={"detail": f"Unknown resource: {resource}"}
)

# Step 3 — build target URL
target_url = f"{base_url}/v1/{path}"

try:
async with httpx.AsyncClient() as client:
response = await client.request(
method=request.method,
url=target_url,
headers={
k: v for k, v in request.headers.items() if k.lower() != "host"
},
content=await request.body(),
params=request.query_params,
follow_redirects=True,
)

return Response(
content=response.content,
status_code=response.status_code,
media_type=response.headers.get("content-type"),
)

except httpx.RequestError as e:
return JSONResponse(
status_code=503, content={"detail": f"Service unavailable: {str(e)}"}
)
6 changes: 3 additions & 3 deletions modules/module-01/REFLECTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Think about it from three angles: the developer who has to change code, the team

> _Your answer:_

---
---Splitting the monolith into separate services makes the system easier to manage and maintain. For developers, changing one service becomes safer because it does not risk breaking unrelated parts of the application. For deployment teams, each service can be updated independently without redeploying the entire system. For users, failures are more isolated, meaning one broken service does not necessarily take down the whole platform

## 2. Your choice

Expand All @@ -36,7 +36,7 @@ What would break, slow down, or become harder to manage if you merged those two

> _Your answer:_

---
---We decided to separate the activity-service from the logging-service. The activity-service is responsible for gameplay actions, while the logging-service only stores logs and audit records. If both services were merged together, gameplay performance could be affected whenever logging becomes slow or unavailable. Keeping them separate also makes the system easier to scale and maintain because logging can grow independently from gameplay activity.

## 3. The tradeoff

Expand All @@ -48,6 +48,6 @@ No need to solve it: just name it honestly. This is exactly the tension the rest

> _Your answer:_

---
---Communication between components was simpler in the monolith because everything was inside the same application and database. In a distributed system, services must communicate over the network using REST APIs or async events, which adds complexity, latency, and more points of failure.

_Keep this file. You will refer back to it during the oral presentation._
60 changes: 54 additions & 6 deletions modules/module-01/exercise.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ Read these two documents before doing anything else:
A bounded context is a part of the system that has a clear responsibility and owns its data exclusively. No other service should reach into its database.

For each bounded context you identify, fill in the table:
| Bounded Context | Responsibilities | Owned Entities | Team |
| --------------- | -------------------------------------------------------- | -------------------------- | -------------- |
| Identity | Manages who users are, handles registration and profiles | User, Session | Platform |
| Game Library | Manages the game catalog and game details | Game, Genre | Content |
| Activity | Tracks player activity and gameplay events | Activity, MatchHistory | Analytics |
| Notification | Sends emails, alerts, and real-time notifications | Notification, QueueMessage | Communication |
| Logging | Stores system logs and audit events | LogEntry, AuditRecord | Infrastructure |

| Bounded Context | Responsibilities | Owned Entities | Team |
| --------------- | -------------------------------------------------------- | -------------- | ----------- |
| Identity | Manages who users are, handles registration and profiles | User, Session | Platform |
| Game Library | _(fill in)_ | _(fill in)_ | _(fill in)_ |
| _(add more)_ | | | |

There is no single correct answer: what matters is that you can justify each row.

Expand All @@ -56,7 +58,21 @@ 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.

---
---Service Contracts
gateway → identity-service
Trigger: user logs in
Protocol: REST
Payload: { email, password }

activity-service → logging-service
Trigger: gameplay activity recorded
Protocol: RabbitMQ event (async)
Payload: { activity_id, user_id, action, timestamp }

game-library-service → notification-service
Trigger: new game added
Protocol: RabbitMQ event (async)
Payload: { game_id, title, release_date }

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

Expand All @@ -70,13 +86,45 @@ Draw the full GameHub service map:
This can be a sketch on paper, a whiteboard photo, or ASCII art committed to your branch.

---
+-------------+
| Gateway |
+-------------+
/ | \
/ | \
v v v

+-------------+ +-------------+
| Identity | | Game Library|
| Service | | Service |
+-------------+ +-------------+
| |
| |
v v

+-------------+ - - - - - - - - -
| Activity | - - > Notification |
| Service | (RabbitMQ) |
+-------------+ - - - - - - - - -

|
|
v

+-------------+
| Logging |
| Service |
+-------------+

## Discussion _(~15 min)_

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?
--Node.js is well suited for real-time communication and event-driven systems.
This shows that microservices can use different technologies depending on the needs of each service.

2. What is the risk of `activity-service` calling `logging-service` synchronously — why might you prefer an async event instead?

3. Why does `logging-service` need a GDPR consent check before recording any activity?

You do not need to write these answers down — they are warm-up for your REFLECTION.md.
Expand Down
37 changes: 37 additions & 0 deletions modules/module-03/REFLECTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Module 3 — Reflection
**Team name**: microservicesWithPython
**Branch**: `module-03/microservicesWithPython`
**Submitted**: before Module 4 lesson

---

## 1. The Gateway
Without a gateway, what would a frontend app need to hardcode?
What happens when a service moves to a different port or machine?

> *Your answer:*
> Without a gateway, the frontend would need to hardcode the address and port of every service — user-service on 8001, game-service on 8002, and so on. If any service moves to a different port or machine, every frontend client breaks and needs to be updated. The gateway acts as a single stable entry point: services can move or be replaced behind it without the frontend ever knowing. It also means we only expose one public address instead of several.

---

## 2. Two calls, two behaviors
User validation blocks the activity from being saved.
Game enrichment is optional — the activity saves regardless.

What is the consequence for the data if you skip user validation?
What is the consequence if you block on a missing game?

> *Your answer:*
> If we skip user validation, we end up saving activities that reference user IDs that don't exist — the database fills with orphaned records that can never be meaningfully displayed or queried. That is a data integrity problem that is hard to clean up later. On the other hand, if we block on a missing game, we reject a valid user action simply because game-service happens to be down or slow. The activity itself is real and should be recorded. Game data is enrichment — nice to have in the response, but not a reason to lose the event entirely. The two risks are different: one is about corrupting data, the other is about unnecessary availability loss.

---

## 3. Chain latency
If three services each take 1 second, how long does the user wait?
What if one of them goes down entirely?

> *Your answer:*
> If three services are called sequentially and each takes 1 second, the user waits at least 3 seconds — latency adds up in a chain. If one service goes down entirely and there is no timeout or fallback, the request hangs until it times out, which could mean the user waits indefinitely or gets a hard error. This is why the game-service call is designed to fail gracefully: it has a bounded retry window and defaults to null instead of blocking forever. For the user validation call, a 503 is returned quickly rather than hanging, so the system stays predictable even under partial failure.

---
*Keep this file. You will refer back to it during the oral presentation.*
15 changes: 15 additions & 0 deletions services/activity-service/app/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker

DATABASE_URL = "sqlite:///./activities.db"

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
74 changes: 74 additions & 0 deletions services/activity-service/app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
import httpx

from app.database import Base, engine, get_db
from app.models import Activity
from app.schemas import ActivityCreate, ActivityOut

Base.metadata.create_all(bind=engine)

app = FastAPI()

USER_SERVICE_URL = "http://localhost:8001"
GAME_SERVICE_URL = "http://localhost:8002"


async def validate_user(user_id: int) -> dict:
"""Call user-service to verify user exists. Retries once on transient failure."""
for attempt in range(2):
try:
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(f"{USER_SERVICE_URL}/v1/users/{user_id}")
if resp.status_code == 404:
raise HTTPException(status_code=404, detail=f"User {user_id} not found")
resp.raise_for_status()
return resp.json()
except HTTPException:
raise
except httpx.RequestError:
if attempt == 1:
raise HTTPException(status_code=503, detail="User service unavailable")


async def fetch_game(game_id: int):
"""Fetch game data — optional enrichment. Returns None on any failure."""
if game_id is None:
return None
try:
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get(f"{GAME_SERVICE_URL}/v1/games/{game_id}")
if resp.status_code == 200:
return resp.json()
return None
except Exception:
return None


@app.post("/v1/activities", response_model=ActivityOut)
async def create_activity(data: ActivityCreate, db: Session = Depends(get_db)):
# Step 1 — validate user (critical, blocks save if fails)
await validate_user(data.user_id)

# Step 2 — save activity
activity = Activity(user_id=data.user_id, game_id=data.game_id, action=data.action)
db.add(activity)
db.commit()
db.refresh(activity)

# Step 3 — enrich with game data (optional, never blocks)
game = await fetch_game(data.game_id)

result = ActivityOut.model_validate(activity)
result.game = game
return result


@app.get("/v1/activities", response_model=list[ActivityOut])
def list_activities(db: Session = Depends(get_db)):
return db.query(Activity).all()


@app.get("/")
def root():
return {"message": "Activity service running"}
12 changes: 12 additions & 0 deletions services/activity-service/app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.sql import func
from app.database import Base

class Activity(Base):
__tablename__ = "activities"

id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, nullable=False)
game_id = Column(Integer, nullable=True)
action = Column(String, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
17 changes: 17 additions & 0 deletions services/activity-service/app/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional, Any

class ActivityCreate(BaseModel):
user_id: int
game_id: Optional[int] = None
action: str

class ActivityOut(BaseModel):
id: int
user_id: int
game_id: Optional[int]
action: str
created_at: datetime
game: Optional[Any] = None
model_config = {"from_attributes": True}
6 changes: 6 additions & 0 deletions services/activity-service/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fastapi
uvicorn[standard]
sqlalchemy
httpx
python-dotenv
aiosqlite
Loading