Module 12 — Microservices
A field note, written to be understood by anyone

What does it actually
mean to build a microservice?

This walks through one small, real, working piece of software, an Orders API from "what's an API?" all the way to a real bug we hit and fixed.

~15 minute read Built with FastAPI & Python Zero prior knowledge needed
Where this fits

Eleven modules led to this one

This is part of a hands-on course. Every module takes code that works fine as a tiny example, and shows why it breaks down once it meets the real world, then teaches the fix.

01Layered ArchitectureOne tangled file, split into clear, separate jobs
02Creational & Structural PatternsTwo payment providers, made to work behind one shared interface
03Behavioral PatternsA swappable pricing engine, plus a notify-everyone system
04Memory & StreamingProcessing a file piece by piece instead of loading it all at once
05Data at ScaleFewer trips to the database, smarter pagination, caching
12MicroservicesToday — the same kind of logic, now reachable over the internet
Starting from zero

What's an API, really?

Two apps need to talk to each other. Neither can just reach inside the other and grab what it wants — so one of them opens a set of specific, agreed-upon "doors" the other is allowed to knock on.

An API is a defined way for one program to ask another to do something, or hand over data.

Think about the Uber app on your phone. It can't reach into Uber's servers directly. Instead, Uber's servers expose a fixed set of doors your phone is allowed to knock on — "get me nearby drivers," "book this ride," "cancel this ride." That whole set of doors, plus the rules for how to knock on each one, is the API.

01GET /drivers
02POST /rides
03DELETE /rides/{id}
The language underneath

Web APIs speak HTTP

A web API is just a program sitting somewhere, waiting for requests over HTTP — the same protocol your browser uses to load any page — and answering back with data.

GET
"Give me something."
Loading a webpage. Fetching a list of orders that already exist.
POST
"Here — take this and create something."
Submitting a form. Creating a brand new order. This module uses exactly this one.
How much do you build at once?

One big app, or many small ones?

A microservice means building many small, independent programs instead of one giant one — each responsible for exactly one job, talking to the others only through APIs.

Monolith

Orders, Payments, and Inventory all live in one codebase. They ship together and scale together — simple, until one part needs to change without the others.

Orders
Payments
Inventory
Microservices

Each piece is its own independent program. One going down doesn't take the rest with it. Only the piece under pressure needs to scale.

Orders
Payments
Inventory
We're building the Orders piece today — on its own, with its own front door.
The tool doing the heavy lifting

FastAPI gets out of your way

FastAPI is a Python tool that turns an ordinary function into something the internet can call, without you having to write the plumbing by hand.

@app.post("/orders") async def create_order(request: CreateOrderRequest):

Decorators map URLs to functions

@app.post("/orders") means "run this function whenever someone POSTs here." That's the whole setup.

Data gets checked automatically

Before your own code runs a single line, FastAPI checks the incoming data is shaped correctly — for free.

You get a ready object, not raw text

No manual parsing of raw internet traffic — the data just arrives, already usable.

The paperwork underneath

Schemas are blueprints, not code

Think of this service as a reception desk. Schemas are the paperwork templates it uses to check exactly what's allowed in the door — and what goes back out.

OrderLineIn

one line item on the order

sku: str
unit_price: float
quantity: int

CreateOrderRequest

the intake form for a new order

customer_id: str
lines: list[OrderLineIn]

OrderResponse

the receipt sent back

customer_id: str
total: float
line_count: int
Reading the code, slowly

Anatomy of one line of code

This single decorator line is doing four separate jobs at once. Here's each one, in plain English.

@app.post("/orders", response_model=OrderResponse, status_code=201) async def create_order(request: CreateOrderRequest):
"/orders"Only run this function when a request hits this exact address
response_model=OrderResponseShape whatever this function returns to match that receipt format
status_code=201The standard internet code meaning "something new was successfully created"
request: CreateOrderRequestThe incoming data is auto-checked and handed to you already usable
A real bug, from earlier in this course

Computers are surprisingly bad at decimals

Before Module 12, an earlier exercise hit a genuine bug — the kind every developer runs into eventually. Converting a dollar amount into cents produced the wrong number.

Before — has a bug
# converts dollars to cents cents = int(amount * 100) result = sdk.make_payment(cents, token)
After — fixed
# converts dollars to cents cents = round(amount * 100) result = sdk.make_payment(cents, token)

What actually happened

The test sent $19.99 and expected it to become exactly 1999 cents. Instead, it got 1998.

assert sdk.calls == [(1999, 'tok_abc')] AssertionError: assert [(1998, 'tok_abc')] == [(1999, 'tok_abc')] At index 0 diff: (1998, 'tok_abc') != (1999, 'tok_abc')

Why: computers store decimal numbers like 19.99 in a format that can't represent every decimal exactly — it's stored as something microscopically smaller, like 19.989999999999998. int() chops off everything after the decimal point, so that tiny error rounds down and loses a whole cent. round() rounds to the nearest whole number instead.

Building it from scratch

Before & after: the endpoint itself

This is what the Orders endpoint looked like as an unfinished stub, versus the finished, working version.

Before — unfinished
@app.post("/orders", response_model=OrderResponse, status_code=201) async def create_order(request): # nothing written yet raise NotImplementedError
After — complete
@app.post("/orders", response_model=OrderResponse, status_code=201) async def create_order(request): if not request.lines: raise HTTPException(422, ...) total = sum(l.unit_price * l.quantity for l in request.lines) _orders[order_id] = {...} return OrderResponse(...)

Added: a guard against empty orders

If nothing was ordered, the function now stops immediately and replies with a clear error — instead of silently accepting nonsense.

Added: the actual price calculation

Price × quantity, summed across every line — the one piece of real math this endpoint needed to do.

Added: saving the order

The finished order gets stored so it can be looked up again later.

Added: sending back a receipt

The customer, the total, and the item count get packaged up and sent back as proof the order went through.

The logic itself

Four steps, in order

Nothing new conceptually here — this is the same shape of logic used throughout the whole course, just now running inside a web request.

01

Reject empty orders

if not request.lines: raise HTTPException(422, ...)
02

Add up the total

total = sum(l.unit_price * l.quantity for l in request.lines)
03

Store it

_orders[order_id] = { "customer_id": ..., "total": total}
04

Hand back a receipt

return OrderResponse( customer_id=..., total=..., line_count=...)
The connection that matters most

Same logic, new clothing

Module 1's plain function and this module's web endpoint do the identical job. Only the wrapping around it changed.

Module 1 — plain function
def place_order(order_id, customer_id, lines): validate_order(lines) total = order_total(lines) repo.save(order_id, ..., total) return f"Order placed: {total}"
=
Module 12 — web endpoint
@app.post("/orders") async def create_order(request): validate lines total = sum(...) _orders[id] = {...} return OrderResponse(...)
Proving it works

Three tests, three jobs

Each test acts like a pretend user of the API, sending a request and checking exactly what comes back.

Happy path
test_create_order_returns_computed_total
A valid 2-item order → status 201, correct total, correct item count.
Your own code
test_create_order_rejects_empty_lines
Zero items → your own guard fires → status 422.
Free, from the schema
test_create_order_rejects_malformed_request
Missing "lines" entirely → FastAPI catches it automatically, no code of yours runs.
Worth sitting with

Questions worth asking

Tap any question to reveal the answer, written in plain language.

What breaks if a request is missing "lines" entirely — and whose code catches it?

Nothing crashes. FastAPI's own schema check catches it automatically, before any of the developer's own code even runs, and replies with a standard "your data isn't shaped right" error.

Why 201 instead of 200 on success?

201 specifically means "a brand new thing was successfully created." 200 is a generic "it worked." 201 is more precise, and tells anyone calling this API that a new order now exists.

Orders are stored in memory. What happens if the service restarts?

Every stored order disappears — memory is wiped clean on restart. A real system would use a proper database instead, the same fix explored back in Module 5.

If a Payments service joins next quarter, does it touch this file at all?

No. Because this service only exposes and uses a defined API, a completely separate Payments service can be built, deployed, and scaled on its own — without this file changing at all.

Putting it all together

What this all adds up to

An API is a contract — a defined way for programs to talk to each other.

A microservice is one small, independent piece of a bigger system.

FastAPI and its schemas give you free structural checks — the business rules are still yours to write.

Even small bugs, like a decimal rounding down instead of properly rounding, can quietly cost real money — which is why tests exist.

The core logic never really changes across this course. Only how it's exposed to the world does.

It's just a front door for logic that already worked.

That's the whole story of Module 12 — take something that already runs correctly, and give it a door the rest of the world can knock on. Notes by Howard · Hands-On Enterprise Python

All slides