๐Ÿ“š Study Notes / Home / HLD / Session 6
Session 06 ยท Microservices & Chat App

From one big app to many small services โ€” and a chat app you could ship

So far we've designed pieces of systems. Today we learn how those pieces talk to each other. We start with the language of the web โ€” HTTP APIs โ€” then split a giant program into small microservices, build a tiny chat application out of them, decide how services should call one another, and finish with the right way to handle image uploads. We assume you've studied none of this before. Every topic starts with a tiny "explain like I'm 5" story, then we go deeper with real code. Take it slow.

โฑ 21 min read๐Ÿ“– 5 topics

1 HTTP APIs & REST design practices


Explain like I'm 5

Imagine a restaurant. You don't walk into the kitchen and cook โ€” you talk to a waiter. You say "I'd like the pasta," the waiter carries your order to the kitchen, and brings back a plate. An API is that waiter: a polite, agreed-upon way to ask a program for something and get an answer back, without ever touching the kitchen. You only need to know what to ask for and how to ask โ€” not how the food is made.

An API (Application Programming Interface) is a contract: a set of requests one program promises to understand, and the responses it promises to give back. When that contract travels over the web, it almost always rides on HTTP (HyperText Transfer Protocol) โ€” the same protocol your browser uses to load pages. A Web API is just an API you reach by sending HTTP requests to a URL.

The shape of an HTTP request

Every HTTP request has four parts: a method (the verb โ€” what you want to do), a URL (what you want to do it to), optional headers (extra info like who you are), and an optional body (the data you're sending). The response comes back with a status code (did it work?), headers, and a body (the answer).

HTTP methods โ€” the verbs

HTTP gives us a small set of verbs. Each says what kind of action you intend, which keeps APIs predictable.

MethodWhat it meansEveryday analogy
GETRead data. Should never change anything.Reading the menu.
POSTCreate something new.Placing a new order.
PUTReplace a thing entirely with what you send.Re-writing the whole order from scratch.
PATCHUpdate part of a thing."Actually, make it extra spicy."
DELETERemove a thing.Cancelling the order.

Status codes โ€” did it work?

The server replies with a three-digit status code. The first digit tells you the category at a glance.

RangeMeaningCommon examples
2xxSuccess200 OK, 201 Created, 204 No Content
3xxRedirection301 Moved Permanently, 304 Not Modified
4xxYou (the client) made a mistake400 Bad Request, 401 Unauthorized, 404 Not Found, 429 Too Many Requests
5xxThe server broke500 Internal Server Error, 503 Service Unavailable

Resources & URLs โ€” the nouns

The dominant style for designing web APIs is REST (REpresentational State Transfer). Its core idea: model everything as resources (nouns), each living at a clean URL, and act on them with HTTP verbs. So you don't invent a verb-y URL like /createUser; you POST to the noun /users.

GoalMethod + URL
List all usersGET /users
Get one userGET /users/42
Create a userPOST /users
Replace user 42PUT /users/42
Update part of user 42PATCH /users/42
Delete user 42DELETE /users/42
A user's messagesGET /users/42/messages

Statelessness

REST APIs are stateless: each request must carry everything the server needs to understand it (like a login token). The server does not remember anything about you between requests. This is exactly the same idea we met with LLMs being stateless โ€” and it's powerful here for the same reason: any server in a fleet can handle any request, so you can add more servers freely (recall scaling from earlier sessions). The "state" (like your session) lives in a token you resend, or in a shared database.

Idempotency

Explain like I'm 5 (idempotency)

Pressing a regular elevator button 5 times is the same as pressing it once โ€” the elevator still comes to your floor exactly once. That's idempotent: doing it again doesn't cause extra stuff to happen.

Idempotency means: making the same request many times has the same effect as making it once. This matters because networks are flaky โ€” if you don't get a reply, you might retry, and you don't want retries to create five identical orders.

  • GET, PUT, DELETE are idempotent: reading twice is fine; replacing user 42 twice leaves the same result; deleting twice still ends with it gone.
  • POST is not idempotent by default: two POSTs to /orders create two orders. The fix is an idempotency key โ€” a unique ID the client sends so the server can recognise a retry and ignore the duplicate.

Versioning

Once real apps depend on your API, you can't just change it โ€” you'd break them. So you publish a new version and keep the old one running. Common approaches: in the URL (/v1/users, /v2/users) or in a header (Accept: application/vnd.myapp.v2+json). URL versioning is the simplest and most common.

Good API design principles

  • Use nouns, not verbs in URLs; let HTTP methods be the verbs.
  • Be consistent: plural nouns (/messages), lowercase, hyphenated paths.
  • Return correct status codes โ€” don't reply 200 with an error inside.
  • Filter/paginate big lists: GET /messages?limit=20&page=2.
  • Version from day one so you can evolve safely.
  • Clear error bodies: tell the caller what went wrong and how to fix it.
Example request & response

Creating a message. Request:

POST /v1/messages HTTP/1.1
Host: api.chatapp.com
Authorization: Bearer abc123
Content-Type: application/json

{
  "chat_id": 7,
  "sender_id": 42,
  "text": "Hello!"
}

Response:

HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/messages/9981

{
  "id": 9981,
  "chat_id": 7,
  "sender_id": 42,
  "text": "Hello!",
  "created_at": "2026-06-20T10:31:00Z"
}

Notice the verb (POST), the noun URL (/v1/messages), the JSON body, the 201 Created code, and the Location header pointing at the new resource. That's textbook REST.

Recap An API is a contract; a web API rides on HTTP. REST models data as resources at clean URLs, acted on by verbs (GET/POST/PUT/PATCH/DELETE), with status codes (2xx/4xx/5xx) reporting the outcome. APIs are stateless; idempotent methods are safe to retry; and you version so you can change things without breaking callers.

2 Monolith vs Microservices


Explain like I'm 5

A monolith is one giant LEGO castle built as a single solid block โ€” beautiful, but if you want to change one window you risk wobbling the whole thing, and only one kid can work on it at a time. Microservices are like building the castle from many small, snap-together rooms. Each room is built by a different kid, and you can replace the kitchen without touching the towers.

A monolith is an application built and deployed as one single unit. All the code โ€” users, messages, billing, search โ€” lives in one codebase, runs in one process, and ships together. Microservices is the opposite approach: you split the application into many small, independent services, each owning one area of the business, each deployed on its own, each talking to the others over the network (usually via the HTTP APIs from Topic 1).

What makes a service "micro"?

Not lines of code โ€” it's about ownership and independence. A good microservice:

  • Owns one area (e.g. "messages") and its own data store.
  • Can be deployed, scaled, and even rewritten without touching the others.
  • Talks to other services only through their public APIs โ€” never by reaching into their database.

Bounded contexts โ€” where to cut

The hard part is deciding the seams. The guiding idea is the bounded context (a term from Domain-Driven Design): a boundary inside which a set of words and rules has one clear meaning. The word "user" might mean a login account in one context and a billing customer in another โ€” those are natural places to split. Cut along business capabilities, not technical layers. "Messaging," "Payments," "Notifications" are good services; "the database layer" is not.

The big idea

Microservices trade simplicity for independence. You accept the real cost of a distributed system (network calls, partial failures, more moving parts) in exchange for teams shipping independently and scaling each piece on its own. It's a tooling-and-organisation decision as much as a technical one.

Side by side

AspectMonolithMicroservices
CodebaseOneMany, one per service
DeploymentAll togetherEach independently
ScalingScale the whole app, even if only one part is hotScale only the busy service
Tech choicesUsually one language/stackEach service can pick its own
Failure blast radiusA bug can take down everythingOne service can fail in isolation
CommunicationIn-process function calls (fast, simple)Network calls (slower, can fail)
DataOne shared databaseA database per service
Team workflowTeams coordinate on one repoTeams own services independently
Operational complexityLowHigh (monitoring, networking, deploys)

When to split (and when not to)

Start with a monolith. For a new product or small team, a monolith is faster to build, easier to debug, and has no network between parts. Split into microservices when you feel real pain:

  • Different parts need to scale very differently (search is hammered; billing is quiet).
  • Multiple teams keep stepping on each other in one codebase.
  • You want to deploy parts on different schedules or in different languages.
  • A bug in one area keeps taking down unrelated features.
Watch out โ€” the distributed tax

Microservices are not "free modularity." A function call that used to be instant and reliable becomes a network request that can be slow, fail, or arrive twice. You now need service discovery, retries, timeouts, distributed tracing, and a database per service. Splitting too early โ€” especially before you understand the domain โ€” is one of the most common architecture mistakes. (We'll see why partial failure is so tricky in the CAP and replication discussion next session.)

Recap A monolith is one deployable unit โ€” simple but coupled. Microservices split the app into independently deployable, independently scalable services, each owning one bounded context and its own data, talking over APIs. You gain independence and pay a distributed-systems tax. Default to a monolith; split when real pain demands it.

3 Building a chat app with microservices


Explain like I'm 5

Think of a post office. One clerk's whole job is to take your letters and deliver them โ€” that's the mail desk. In a back room, someone quietly keeps a tally: how many letters were sent today, who's the busiest sender โ€” that's the counting desk. The mail desk never waits on the counting desk; counting happens calmly in the background. Our chat app is built the same way: one service delivers messages, another counts and analyses them.

Let's design a simple chat application as two microservices. Keeping it to two makes the boundaries crystal clear and shows off the bounded-context idea from Topic 2.

The two services

ServiceOwnsResponsibilities
Message Service The messages themselves + its own messages database Accept a new message, validate it, store it, return it; list a chat's recent messages; deliver to recipients. This is the critical, user-facing path โ€” it must be fast and reliable.
Analytics Service Counts, stats, trends + its own analytics store Track messages sent per user/per chat, active users, busiest hours. This is a background concern โ€” it can be a little slow or briefly behind without hurting the chat experience.

How they fit together

๐Ÿ“ฑ
Client
User sends a message
โ†’
โœ‰๏ธ
Message Service
Validate & store
โ†’
๐Ÿ“จ
Event
"message.sent" emitted
โ†’
๐Ÿ“Š
Analytics Service
Update counts in background

The key design choice: the Message Service does not phone the Analytics Service and wait. It just stores the message, replies to the user immediately, and emits an event โ€” a small "this happened" note. The Analytics Service picks that up on its own time. If analytics is slow or down, chatting still works perfectly. (We'll formalise this synchronous-vs-asynchronous choice in Topic 4.)

A real endpoint โ€” the Message Service

Here's a small FastAPI service (FastAPI is a popular Python web framework) exposing the POST /v1/messages endpoint we designed in Topic 1.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from datetime import datetime, timezone

app = FastAPI()

# In a real service this is a database; a dict keeps the demo simple.
messages = {}
next_id = 1

class NewMessage(BaseModel):     # validates the request body
    chat_id: int
    sender_id: int
    text: str

def emit_event(name, payload):
    # Publish to a message queue (Topic 4). Fire-and-forget.
    print(f"event: {name} -> {payload}")

@app.post("/v1/messages", status_code=201)
def create_message(msg: NewMessage):
    global next_id
    if not msg.text.strip():
        raise HTTPException(status_code=400, detail="text required")

    record = {
        "id": next_id,
        "chat_id": msg.chat_id,
        "sender_id": msg.sender_id,
        "text": msg.text,
        "created_at": datetime.now(timezone.utc).isoformat(),
    }
    messages[next_id] = record
    next_id += 1

    # Tell the world a message was sent โ€” Analytics will react later.
    emit_event("message.sent", {"chat_id": msg.chat_id,
                                "sender_id": msg.sender_id})
    return record        # FastAPI sends it back as JSON with 201

@app.get("/v1/chats/{chat_id}/messages")
def list_messages(chat_id: int, limit: int = 20):
    chat_msgs = [m for m in messages.values() if m["chat_id"] == chat_id]
    return chat_msgs[-limit:]
What the Analytics Service does

The Analytics Service runs separately. It consumes the message.sent events (off a queue) and just increments counters in its own database โ€” something like messages_per_user[42] += 1. It then exposes its own read API, e.g. GET /v1/stats/users/42, returning {"messages_sent": 1287}. Two services, two databases, one event flowing between them.

Worked example โ€” a message's journey
  1. Alice's phone sends POST /v1/messages with her text.
  2. Message Service validates (non-empty), saves it, returns 201 with the saved message โ€” Alice sees a delivered tick in < 100 ms.
  3. In the same step it emits message.sent to the queue, then forgets about it.
  4. Seconds later, the Analytics Service reads that event and bumps Alice's "messages sent today" count.
  5. If Analytics was down for an hour, the events wait in the queue and get processed when it's back โ€” Alice never noticed.
Recap A chat app splits cleanly into a Message Service (the fast, critical path: validate, store, return) and an Analytics Service (background counting). The Message Service stays fast by emitting an event instead of waiting on Analytics. Each service owns its own data and exposes its own REST API.

4 Inter-service communication


Explain like I'm 5

Two ways to ask a friend for help. You can phone them and stand there waiting for the answer before you do anything else โ€” that's synchronous. Or you can leave a sticky note on their door and walk away; they'll deal with it when they can โ€” that's asynchronous. Both are useful; you pick based on whether you actually need the answer right now.

Once you have many services (Topic 2), they must talk. There are two big questions: what format do they use, and do they wait for the reply?

What format: HTTP/REST vs RPC (gRPC)

We met REST over HTTP in Topic 1 โ€” human-friendly URLs and JSON. An alternative is RPC (Remote Procedure Call): you call a function on another service as if it were a local function (analytics.GetUserStats(42)) and the framework handles the network. The most popular modern RPC framework is gRPC (from Google), which sends compact binary data (using Protocol Buffers) over a fast connection.

AspectREST / HTTP + JSONgRPC (RPC)
Data formatJSON (text, human-readable)Protocol Buffers (binary, compact)
Speed / sizeSlower, largerFaster, smaller
ReadabilityEasy to read & debug by handNeeds tooling to inspect
ContractLoose (docs/conventions)Strict .proto schema, code-generated
Best forPublic APIs, browser clients, simple internal callsHigh-traffic internal service-to-service calls

Do they wait: synchronous vs asynchronous

This is the deeper choice. Both REST and gRPC are usually synchronous request/response: you call, you block, you get an answer. Asynchronous communication instead uses a message queue (a buffer in the middle, like RabbitMQ, Kafka, or AWS SQS): the sender drops a message and moves on; a receiver picks it up later. This is the "sticky note" model โ€” exactly what our chat app used to feed the Analytics Service.

๐Ÿ“ค
Producer
Drops a message
โ†’
๐Ÿ“ฅ
Queue
Holds it safely
โ†’
โš™๏ธ
Consumer
Processes when ready
AspectSynchronous (REST/gRPC)Asynchronous (queue)
Caller waits?Yes โ€” blocks for the replyNo โ€” fire and forget
CouplingTight: callee must be up nowLoose: callee can be down/slow; messages buffer
Failure handlingCaller sees errors immediatelyQueue absorbs spikes & outages; retries built in
Gets a return value?Yes, right awayNot directly โ€” results come via another event
Best for"I need the answer to continue" (e.g. check a password)"Just go do this eventually" (e.g. analytics, emails, thumbnails)
When to use each โ€” our chat app
  • Synchronous: The Message Service must ask an Auth Service "is this token valid?" before storing the message. It needs the answer now, so it makes a blocking REST/gRPC call.
  • Asynchronous: Updating analytics counts can happen later, so the Message Service drops a message.sent event on a queue and doesn't wait.

Rule of thumb: if you need the result to proceed, go synchronous; if it can happen "eventually," go asynchronous.

Trade-off โ€” the cost of waiting

Chains of synchronous calls are fragile: if service A calls B calls C and C is slow, A is stuck too โ€” one slow service can stall a whole request, and a cascade of timeouts can take the system down. Async queues add resilience (they buffer spikes and outages) but cost you simplicity: there's no immediate answer, and you must handle messages that arrive twice or out of order. This loose coupling leads to eventual consistency โ€” the analytics count is briefly stale โ€” a theme we explore fully next session.

Recap Services choose a format โ€” REST/JSON (readable, universal) or gRPC (fast, binary, strict) โ€” and a style โ€” synchronous request/response (you wait; use when you need the answer to continue) or asynchronous queues (fire-and-forget; use for background work). Sync is simple but couples services tightly; async adds resilience but only eventual results.

5 Handling image uploads in messages


Explain like I'm 5

Imagine your address book. You don't glue an entire photo of your friend's house onto the tiny contact card โ€” that would make the book fat, slow, and impossible to flip through. Instead you write down the address of the house. The photo (the big thing) lives somewhere roomy; the card just holds a pointer to it. Databases work the same way with images: store the picture elsewhere, keep only its address in the message.

People send photos in chat. The naive idea is to shove the image bytes straight into the messages database. Don't. A raw image file is a blob (Binary Large OBject) โ€” and databases are terrible at storing big blobs.

Why not store blobs in the database?

  • Bloat & slowness: images are huge compared to text. The database balloons, backups take forever, and queries that used to be instant get sluggish.
  • Expensive: database storage and memory are far pricier per gigabyte than plain file storage.
  • Bad at serving: a database can't efficiently stream a 4 MB photo to a browser the way a purpose-built file server can.
  • Wasted cache: the database's memory should hold hot rows, not photo bytes.

The right way: object storage + CDN + a URL

Put the actual image in object storage โ€” a service built to hold arbitrary files cheaply and durably (Amazon S3, Google Cloud Storage, Azure Blob Storage). Then store only a small URL / reference in the message row. To make downloads fast worldwide, serve those files through a CDN (Content Delivery Network) โ€” a fleet of edge servers that cache copies of your files physically close to users (recall caching from earlier sessions; a CDN is caching for static files).

Key takeaway

The database stores a pointer, not the payload. A message row becomes {"text": "look!", "image_url": "https://cdn.chatapp.com/img/abc123.jpg"}. The heavy bytes live in S3 and are served via the CDN โ€” keeping the database small, fast, and cheap.

The upload flow

The clean pattern is a pre-signed URL: the client uploads the image directly to object storage (not through your service), so big bytes never clog your servers. Here's the flow.

๐Ÿ“ฑ
1. Ask
Client requests upload slot
โ†’
๐ŸŽซ
2. Pre-sign
Service returns S3 pre-signed URL
โ†’
โฌ†๏ธ
3. Upload
Client PUTs image straight to S3
โ†’
๐Ÿ’พ
4. Save URL
Client POSTs message with image_url
โ†’
๐ŸŒ
5. Serve
Others fetch via CDN
Worked example โ€” the endpoint that hands out an upload slot

Step 2 in code. The Message Service asks S3 for a short-lived, signed URL and gives it to the client.

import boto3, uuid
from fastapi import FastAPI

app = FastAPI()
s3 = boto3.client("s3")
BUCKET = "chatapp-images"

@app.post("/v1/uploads")
def create_upload(content_type: str = "image/jpeg"):
    key = f"img/{uuid.uuid4().hex}.jpg"   # unique file name

    # A temporary URL the client can PUT the image to directly.
    upload_url = s3.generate_presigned_url(
        "put_object",
        Params={"Bucket": BUCKET, "Key": key,
                "ContentType": content_type},
        ExpiresIn=300,                      # valid for 5 minutes
    )

    return {
        "upload_url": upload_url,         # client PUTs the bytes here
        # the public address to store in the message later:
        "image_url": f"https://cdn.chatapp.com/{key}",
    }

The client then uploads the file with a plain PUT to upload_url, and finally sends POST /v1/messages with image_url in the body โ€” the same endpoint from Topic 3, now carrying a pointer instead of bytes.

Nice bonuses of this pattern

Because the upload skips your service, your API stays light. You can also fire an async event (Topic 4) to a worker that makes thumbnails or scans for malware in the background โ€” and the CDN means a user in Tokyo downloads from a nearby edge server, not your origin in Virginia.

Recap Never store image blobs in the database โ€” they bloat it, slow it, and cost too much. Put the file in object storage (S3), serve it via a CDN, and keep only a URL reference in the message. The clean upload flow uses a pre-signed URL so clients upload directly to storage, keeping your services lean.

โ˜… Putting it all together


You just learned how the pieces of a modern system talk to each other and how to build one. Here's the one-paragraph story connecting all five topics:

Programs talk over HTTP APIs, and the cleanest style is REST โ€” resources at URLs, acted on by verbs, with honest status codes, statelessness, idempotency, and versioning. When one big monolith grows painful, we split it into independently deployable microservices along bounded contexts. We built exactly that for a chat app: a fast Message Service and a background Analytics Service. Those services communicate either synchronously (REST/gRPC, when you need the answer now) or asynchronously via a message queue (when work can happen later). And when users send photos, we never stuff blobs in the database โ€” we put them in object storage, serve them through a CDN, and keep just a URL in the message.

Quick self-check

Which HTTP method would you use to create a new message, and what success code should it return?

POST to the /messages resource, returning 201 Created (ideally with a Location header pointing at the new message).

Why is PUT idempotent but POST is not?

PUT replaces a resource with what you send, so repeating it leaves the same end state. POST creates a new thing each time, so repeating it creates duplicates โ€” which is why you guard POSTs with an idempotency key.

You're building a brand-new product with a two-person team. Monolith or microservices?

Start with a monolith. It's simpler, faster to build, and has no network between parts. Split into microservices later, when real pain (independent scaling, multiple teams, separate deploys) demands it.

Why does the Message Service emit an event instead of calling the Analytics Service directly?

So chatting stays fast and reliable. Analytics is background work โ€” using an asynchronous queue means the Message Service never waits on it, and if Analytics is down the events simply buffer and get processed later.

When would you choose gRPC over REST/JSON between two services?

For high-traffic internal service-to-service calls where speed and small payloads matter. gRPC uses compact binary Protocol Buffers and a strict schema. REST/JSON wins for public APIs, browser clients, and easy debugging.

A teammate wants to store user photos as bytes in the messages table. What's wrong, and what's better?

Blobs bloat the database, slow queries and backups, and cost more โ€” databases aren't built to serve big files. Better: upload the image to object storage (e.g. S3) via a pre-signed URL, serve it through a CDN, and store only the URL in the message row.

๐Ÿ“š References & Further Reading


Class material

Papers, docs & deep dives