The previous five lessons have laid the foundations: what a distributed system is, which models describe it, what you gain and lose by distributing, which fallacies need to be banished and why time is a problem. This closing lesson puts them to work on the case that will stay with us for the rest of the course. We are going to examine the Kilometre Zero monolith in detail, the specific symptoms pushing it to change and the target architecture we will build module by module.
It is important to understand the nature of this lesson: it is a map, not the journey. No specific technology is explained here; we decide which pieces the platform needs, why, and in which lesson of the course each one is developed. We will also set up the minimal working environment (the folder structure of the km0/ project and a docker-compose.yml with just PostgreSQL and the Python application), which will be the starting point on which services will be added.
Contents
- Inside the Kilometre Zero monolith
- The symptoms forcing the evolution
- Design principles for the transition
- The target architecture
- Mapping the architecture to the course
- A gradual path, not a "big bang"
- Setting up the working environment: the
km0/project - Common mistakes and tips
- Design exercises
- Conclusion
- Inside the Kilometre Zero monolith
Recall the starting point (lesson 01-01): a Python application in a single process, a PostgreSQL database and a single server. Let's now look at it in more detail, because to decide how to split it up we have to understand how it is built.
Internal modules
The code is organised into six Python modules, which correspond to the functional domains of the business:
| Module | Responsibility | Main tables | Who uses it |
|---|---|---|---|
catalog |
Products, producers, categories, photos, search | products, producers, categories |
Customers (many reads), producers (few writes) |
orders |
Basket, order creation and life cycle | orders, order_lines |
Customers, customer support |
inventory |
Stock per product and per producer, reservations | stock, stock_movements |
orders, producers |
payments |
Charges through the external gateway, refunds | payments, refunds |
orders |
delivery |
Courier assignment, routes, real-time position | deliveries, couriers, positions |
Couriers (mobile app), customers (tracking) |
analytics |
Sales reports, best-selling products, demand forecasting | Queries over all of the above | Management, producers |
How they interact
Interactions between modules are Python function calls, and operations that touch several modules rely on a single database transaction. The flow for confirming an order is the clearest example:
sequenceDiagram
participant C as Customer (Anna)
participant P as orders
participant I as inventory
participant PA as payments
participant DB as PostgreSQL
C->>P: confirm_order(basket)
P->>DB: BEGIN
P->>I: reserve_stock(lines)
I->>DB: UPDATE stock ...
P->>PA: charge(amount, card)
PA->>PA: call to external gateway (HTTPS)
PA->>DB: INSERT payment
P->>DB: INSERT order
P->>DB: COMMIT
P-->>C: order confirmed
Notice a detail that will prove decisive: the call to the external payment gateway happens inside the database transaction. If the gateway takes 8 seconds, the transaction (and the locks on the stock rows) lasts 8 seconds. And if the gateway fails, the whole transaction is rolled back, which is correct, but in the meantime it has been locking the stock for every other customer.
Deployment
A single artefact (a Python package) that is installed on the server with a script. Deploying means stopping the process, installing and starting up again: about 40 seconds of total unavailability. Deployments happen on Tuesday and Thursday nights, with all the changes from all the teams accumulated since the previous deployment.
- The symptoms forcing the evolution
There is no single reason to transform Kilometre Zero, but five distinct symptoms, each of which pushes towards a different piece of the target architecture. It is worth identifying them precisely because each symptom justifies a specific decision, and decisions that do not respond to any symptom are gratuitous complexity.
Symptom 1: campaign spikes bring everything down
We have already seen it: Grape Harvest Week multiplied traffic by 15 and the server went down completely. The post-mortem showed that 92% of requests were catalog reads (listings, searches, product pages with photos). The rest of the platform went down because it shared a process and a database with the catalog.
What it calls for: being able to scale the catalog independently, serving reads from a cache and photos from separate storage, and isolating failures so that a catalog overload does not affect orders or delivery.
Symptom 2: a payments failure that brought everything down
One Tuesday, the external payment gateway started responding with a 30-second delay. Each order confirmation kept a transaction open (and stock locks held) for those 30 seconds. Within four minutes, PostgreSQL hit its connection limit and the whole platform, including catalog pages that have nothing to do with payments, stopped responding. A failure at an external provider turned into a total outage.
What it calls for: making payments an isolated component with its own resources, containing its slowness or failure (timeouts, graceful degradation), and ensuring that order confirmation does not depend on a single transaction spanning external systems.
Symptom 3: teams tread on each other's toes when deploying
Kilometre Zero already has four teams (catalog and producers, orders and payments, delivery, data). Every deployment carries everyone's changes, so a bug in the delivery module forces the catalog improvements to be rolled back too. The delivery team wants to deploy several times a day; the payments team, which is subject to audits, wants long, controlled cycles. The result is that nobody deploys as often as they need to, and every Tuesday deployment is a tense event.
What it calls for: independent deployment units per domain, with their own cycles, tests and owners, and a deployment platform that makes zero-downtime deployments possible.
Symptom 4: real-time courier telemetry
Kilometre Zero has gone from 12 to 140 couriers, and each one sends their position every 5 seconds: 28 positions per second, 2.4 million a day, which are inserted into the positions table of the same database that handles orders. The table grows by 800 MB a day and the inserts compete with order transactions. On top of that, customers want to see the courier moving on the map in real time, which under the request-response model forces the app to poll every few seconds, multiplying the load.
What it calls for: a different communication channel for high-frequency event streams (not a relational table), asynchronous, bidirectional communication with the apps, and storage suited to high-volume time series.
Symptom 5: sales analytics chokes production
Every night, the analytics module runs heavy queries against the order and stock tables to generate reports. The queries take 3 hours and, during that time, the database is slow for everyone. Producers are asking for richer reports (seasonal demand forecasting, comparisons between cities) that the data team does not dare to implement because every new query puts production at risk.
What it calls for: separating the analytical workload from the transactional one, with a copy of the data in a system designed for large-scale processing, fed by the events the other services produce.
- Design principles for the transition
Before drawing the target architecture, let's set out the principles that follow from this module and will guide every decision:
- Each service owns its data. No service accesses another's tables directly. If
analyticsneeds the orders, it receives them as events or through theordersAPI. This is what makes independent deployment and scaling possible (symptoms 1 and 3), and what forces us to give up the single transaction (which is replaced by the techniques of Module 3). - Failure isolation. A failure or an overload in one service must not spread to the others (symptoms 1 and 2). This requires separate resources, timeouts on every remote call (lesson 01-04) and graceful degradation: if
deliverygoes down, people can still buy; ifpaymentsgoes down, people can still browse the catalog. - Synchronous only when the answer is needed now. Checking the stock before confirming an order needs an immediate answer: synchronous. Telling
analyticsthat an order has been created does not: asynchronous, through events. Every synchronous call avoided is one less network boundary in the availability chain (lesson 01-03). - Take the opposite of each fallacy as true. The network will fail, it will have latency, it will not be secure, the topology will change. Every communication between services carries a timeout, idempotent retries, encryption and dynamic discovery.
- Observability from the start. With six services, without metrics, correlated logs and distributed traces there is no way of knowing what is going on. It is not a later add-on: it is part of the design.
- Gradual evolution. Not everything is rewritten at once. One service is extracted and verified, then the next one is extracted. The monolith keeps running throughout the transition.
- The target architecture
With the symptoms and the principles on the table, this is the platform the course will build:
flowchart TB
subgraph Clients
WEB[Web browser]
APP[Customer mobile app]
COU[Courier app]
end
GW[API Gateway<br/>authentication, rate limiting]
WEB --> GW
APP --> GW
COU -->|MQTT / WebSocket| RT[Real-time server]
subgraph Services["Services (Kubernetes)"]
CAT[catalog]
ORD[orders]
INV[inventory]
PAY[payments]
DEL[delivery]
ANA[analytics]
end
GW --> CAT
GW --> ORD
GW --> DEL
ORD -->|synchronous gRPC| INV
ORD -->|synchronous gRPC| PAY
RT --> DEL
subgraph Bus["Event bus (Kafka)"]
T1[(orders.events)]
T2[(delivery.positions)]
T3[(inventory.events)]
end
ORD -.->|OrderCreated, OrderPaid| T1
INV -.->|StockUpdated| T3
DEL -.->|PositionUpdated| T2
T1 -.-> ANA
T2 -.-> ANA
T3 -.-> CAT
T1 -.-> DEL
subgraph Data["Storage per service"]
PGC[(PostgreSQL<br/>catalog)]
RED[(Redis<br/>catalog cache)]
OBJ[(Object storage<br/>photos)]
CAS[(Cassandra<br/>orders)]
PGI[(Replicated PostgreSQL<br/>inventory)]
PGP[(PostgreSQL<br/>payments)]
TS[(Cassandra<br/>positions)]
DL[(Data lake + Spark/Flink<br/>analytics)]
end
CAT --> PGC
CAT --> RED
CAT --> OBJ
ORD --> CAS
INV --> PGI
PAY --> PGP
PAY -->|HTTPS| EXT[External payment gateway]
DEL --> TS
ANA --> DL
subgraph CrossCutting["Cross-cutting"]
OBS[Observability<br/>Prometheus, Grafana, OpenTelemetry]
SEC[Security<br/>OIDC, mTLS, secrets]
end
Let's walk through the decisions, each one tied to its symptom:
The six services
The service boundaries coincide with the modules of the monolith, and that is no accident: the modules already reflected the business domains and the teams. A good modular monolith is the best preparation for a services architecture. Each service has its own deployment, its own scaling and its own storage (symptoms 1, 2 and 3).
Synchronous communication: gRPC
For interactions that need an immediate answer (orders → inventory to reserve stock; orders → payments to charge), we will use RPC with gRPC: typed calls, with an explicit contract and efficient serialization. Every call carries a timeout, and operations with side effects are idempotent (Module 2).
Asynchronous communication: events over Kafka
For everything that does not need an immediate answer, services publish events on a bus (Kafka): OrderCreated, OrderPaid, StockUpdated, PositionUpdated. Whoever is interested subscribes: analytics consumes everything; catalog consumes StockUpdated to show availability; delivery consumes OrderPaid to plan the delivery. This decouples the services in time (if analytics is down, the events wait on the bus) and solves symptom 5 (the data reaches analytics without querying the production database) and part of symptom 4 (positions are an event stream, not rows in a transactional table).
Storage per service
Each service chooses the storage that best fits its workload:
| Service | Storage | Why |
|---|---|---|
catalog |
PostgreSQL + Redis as a cache + object storage for photos | Many repeated reads (cache), simple relational data, large photos kept out of the database (symptom 1) |
orders |
Cassandra | High write volume, queries by customer and by date, the need to scale horizontally and for very high availability |
inventory |
Replicated PostgreSQL | Needs strong consistency (never sell the last unit twice) and transactions; replication provides availability |
payments |
PostgreSQL | Transactions, auditing, moderate volume |
delivery |
Cassandra (positions, time series) + PostgreSQL (assignments) | 2.4 million positions a day; massive writes keyed by courier and time (symptom 4) |
analytics |
Data lake on object storage + batch processing (Spark) and stream processing (Flink/Kafka Streams) | Large-scale queries kept apart from production (symptom 5) |
The consequence of this decision is that the single order-confirmation transaction disappears: reserving stock (the inventory PostgreSQL), charging (the payments PostgreSQL) and creating the order (Cassandra) are three operations on three systems. Coordinating them without a global transaction is the problem addressed by sagas (03-05).
Real time for couriers
The courier apps send positions over MQTT (a lightweight protocol for mobile networks) and customers receive updates over WebSocket, through a real-time server that publishes to the bus and feeds from it (symptom 4; lessons 02-01 and 08-02).
Analytics: batch and streaming
analytics stops querying production. The events on the bus are stored in a data lake, on top of which run batch jobs (nightly reports, with Spark) and stream processing (real-time campaign metrics, with Flink or Kafka Streams), orchestrated by a scheduler (symptom 5; Module 5).
Security, observability, deployment
A gateway authenticates users (OIDC) and limits traffic; services authenticate to one another with mTLS; secrets are managed centrally (Module 6). Metrics, logs and distributed traces are collected from all the services (Module 7). Everything is deployed as containers on Kubernetes, with a cloud provider, with infrastructure defined as code (07-05, 08-03).
- Mapping the architecture to the course
This table is the compass for the course: each component of the target architecture, and the lesson in which it is built or explained.
| Component / decision | Module | Lesson |
|---|---|---|
| Sockets, TCP/UDP, HTTP, MQTT for couriers | 2 | 02-01 |
| Synchronous calls between services (RPC) | 2 | 02-02 |
gRPC and contract-based serialization (orders → inventory, payments) |
2 | 02-03 |
| Kafka event bus, RabbitMQ queues | 2 | 02-04 |
| Idempotency, outbox, dead-letter queues, competing consumers | 2 | 02-05 |
| What guarantees each storage system gives (consistency models) | 3 | 03-01 |
Why inventory (consistency) and orders (availability) choose differently: CAP and PACELC |
3 | 03-02 |
| How the replicated PostgreSQL leader is elected and how Kafka coordinates: consensus (Raft) | 3 | 03-03 |
Replication of the inventory PostgreSQL; conflict detection |
3 | 03-04 |
| Confirming an order without a global transaction: sagas | 3 | 03-05 |
| How Cassandra distributes orders and positions: partitioning and consistent hashing | 4 | 04-01 |
| Distributed file systems for the data lake | 4 | 04-02 |
| Product photos in object storage (S3/MinIO) | 4 | 04-03 |
Cassandra for orders and for the delivery positions |
4 | 04-04 |
| Redis as the catalog cache | 4 | 04-05 |
Distributed computing models for analytics |
5 | 05-01 |
| Batch reports: MapReduce and Hadoop | 5 | 05-02 |
| Demand forecasting with Spark | 5 | 05-03 |
| Real-time campaign metrics: Flink / Kafka Streams | 5 | 05-04 |
| Orchestrating nightly jobs with Airflow | 5 | 05-05 |
| Customer authentication and roles (JWT) | 6 | 06-01 |
| Encryption of payment data and positions | 6 | 06-02 |
| User and producer identity (OAuth2/OIDC) | 6 | 06-03 |
| mTLS between services, secrets management | 6 | 06-04 |
| Gateway, rate limiting, auditing | 6 | 06-05 |
| Metrics with Prometheus and Grafana | 7 | 07-01 |
| Centralized logs and traces with OpenTelemetry | 7 | 07-02 |
| PostgreSQL failover, Flink checkpoints | 7 | 07-03 |
Timeouts, retries, circuit breaker on orders → payments |
7 | 07-04 |
| Deployment on Kubernetes, Ansible | 7 | 07-05 |
| Testing and chaos engineering | 7 | 07-06 |
| Microservices: boundaries, contracts, organisation | 8 | 08-01 |
| WebSockets and MQTT for real-time tracking | 8 | 08-02 |
| Deployment on AWS/GCP | 8 | 08-03 |
| Serverless functions for photo resizing; edge for the markets | 8 | 08-04 |
| Kilometre Zero end to end | 8 | 08-05 |
- A gradual path, not a "big bang"
The target architecture is the destination, not the first step. Rewriting everything in one go is the surest way to fail: for months there would be nothing to deploy, and at the end everything would have to be migrated at once. Instead, Kilometre Zero will follow the pattern known as the strangler fig: one service is extracted from the monolith, traffic is redirected to it and the monolith loses that responsibility; then the same is done with the next one. At every step, the system works and can be deployed.
| Phase | What is extracted | Symptom it solves | Course modules |
|---|---|---|---|
| 1 | catalog (with Redis and photo storage) |
1: campaign spikes | 2, 4 |
| 2 | payments (isolated, with timeouts and a circuit breaker) |
2: payments failure that brings everything down | 2, 7 |
| 3 | delivery with the event bus and the real-time channel |
4: telemetry | 2, 4, 8 |
| 4 | orders and inventory (with sagas and replication) |
3: independent deployments | 3, 4 |
| 5 | analytics on top of the bus and the data lake |
5: analytics | 5 |
| Cross-cutting | Security, observability, Kubernetes | All | 6, 7 |
We start with the catalog because it causes the most serious symptom, has the fewest dependencies (reads only) and carries the least risk: if the new service fails, traffic is pointed back at the monolith.
- Setting up the working environment: the
km0/ project
km0/ projectTo follow the course hands-on, we will create a project that we will keep extending. In this module it contains only the starting point: the monolith and its database. The monolith is not meant to be implemented in detail; a skeleton that starts up is enough, so that in the following modules we have somewhere to add the services.
Folder structure
km0/
├── docker-compose.yml # Local infrastructure: it will grow module by module
├── README.md
├── monolith/ # The starting point (it will gradually be emptied)
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── app.py # Application entry point
│ ├── catalog/ # One Python package per domain module
│ ├── orders/
│ ├── inventory/
│ ├── payments/
│ ├── delivery/
│ └── analytics/
├── services/ # Empty for now. Module 2: catalog/, orders/ ...
├── infra/ # Empty for now. Module 7: kubernetes/, ansible/
└── sql/
└── 001_initial_schema.sql # The monolith's tablesThe services/ and infra/ folders are empty on purpose: they exist so that the evolution is visible. As each service is extracted, it will appear in services/ and disappear from monolith/.
A minimal docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: km0
POSTGRES_USER: km0
POSTGRES_PASSWORD: km0_dev # for local development only
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d # runs the .sql files when the DB is created
healthcheck:
test: ["CMD-SHELL", "pg_isready -U km0 -d km0"]
interval: 5s
timeout: 3s
retries: 10
monolith:
build: ./monolith
environment:
DATABASE_URL: postgresql://km0:km0_dev@postgres:5432/km0
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy # waits until PostgreSQL responds
volumes:
postgres_data:An explanation of the parts that matter for the course:
servicesdeclares two containers:postgresandmonolith. Docker Compose creates an internal network on which each service is reachable by its name: that is why the connection URL usespostgresas the host, rather than an IP (remember fallacy 5: the topology changes; names are stable, addresses are not).healthcheckanddepends_on ... condition: service_healthyare the first practical example of "do not assume the other node is ready": the monolith does not start until PostgreSQL responds topg_isready. Without this, the monolith would start, try to connect, fail and die. This pattern will be repeated with every service we add.volumespersists the PostgreSQL data across container restarts (the crash-recovery failure model: the process dies, the data on disk survives)../sql:/docker-entrypoint-initdb.druns the initial schema the first time the database is created.
The monolith skeleton
monolith/app.py is deliberately minimal: it checks that it can talk to the database and exposes a health endpoint. It uses only the standard library plus psycopg, so as not to introduce dependencies that will be chosen in later modules.
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
import psycopg
DATABASE_URL = os.environ["DATABASE_URL"]
def count_products() -> int:
"""Minimal query to check that the database responds."""
with psycopg.connect(DATABASE_URL) as conn:
with conn.cursor() as cur:
cur.execute("SELECT count(*) FROM products")
return cur.fetchone()[0]
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/health":
body = {"status": "ok", "products": count_products()}
self._respond(200, body)
else:
self._respond(404, {"error": "not found"})
def _respond(self, code: int, body: dict) -> None:
data = json.dumps(body).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
if __name__ == "__main__":
print("Kilometre Zero (monolith) listening on :8000")
HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()monolith/requirements.txt contains a single line, psycopg[binary]>=3.1, and this is monolith/Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]And sql/001_initial_schema.sql, with a minimal schema and the producers used in the course:
CREATE TABLE producers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
city TEXT NOT NULL
);
CREATE TABLE products (
id SERIAL PRIMARY KEY,
producer_id INTEGER NOT NULL REFERENCES producers(id),
name TEXT NOT NULL,
price NUMERIC(10, 2) NOT NULL, -- never float for money (fallacy 8)
stock INTEGER NOT NULL DEFAULT 0
);
INSERT INTO producers (name, city) VALUES
('La Vega Farm', 'Lleida'),
('Montblanc Dairy', 'Girona'),
('Roble Alto Winery', 'Tarragona');
INSERT INTO products (producer_id, name, price, stock) VALUES
(1, 'Pink tomato', 3.20, 120),
(1, 'Zucchini', 2.10, 80),
(2, 'Aged sheep''s cheese', 14.50, 5),
(2, 'Fresh cheese', 6.80, 30),
(3, 'Crianza red wine', 9.90, 200);To start the environment:
And, in another terminal, check that the monolith responds:
The response should be {"status": "ok", "products": 5}.
That sets up the starting point: exactly the monolith from lesson 01-01, now runnable in containers. In Module 2, services/catalog/ will appear along with a second entry in docker-compose.yml.
Common Mistakes and Tips
- Splitting the monolith by technical layers instead of by domains. Creating a "database service", a "logic service" and an "API service" isolates nothing: every operation still goes through all three. The right boundaries are the business ones (
catalog,orders...), which is where coupling is lowest. - Extracting services that share a database. If
ordersandinventoryare deployed separately but read and write the same tables, they are not two services: they are a distributed monolith, with all the costs of distribution and none of its advantages. - Making every communication synchronous. It turns every flow into a chain of multiplied availabilities and added latencies. The question for each interaction should be "do I need the answer now?"; if not, it is an event.
- Making every communication asynchronous. The opposite extreme fails too: checking the stock through events, waiting for the answer to arrive "eventually", makes it impossible to give the customer an immediate confirmation. Synchronous communication exists for a reason.
- Choosing technologies before problems. "We're going to use Kafka and Cassandra" is not an architecture decision unless it responds to a symptom. Every piece of the target architecture is justified by one of the five symptoms; if you cannot name the symptom, remove the piece.
- Leaving observability and security until the end. When there are six services and something fails, it is too late to start wondering how to correlate logs. Both are part of the first service you extract.
- Tip: keep an architecture decision log (one ADR per decision: context, options, decision, consequences). A year from now, when someone asks "why Cassandra for orders?", the answer will be written down.
Exercises
Exercise 1: Identifying service boundaries
The team proposes adding two features: (a) customer ratings and reviews of products, and (b) per-campaign discount coupons, which are applied to the order total and have a maximum number of uses. For each one, decide whether it should be a new service or part of an existing one, state which data it would own and which communications (synchronous or asynchronous) it would need with the rest. Justify each decision with the principles from section 3.
Exercise 2: Choosing the type of communication
For each interaction, say whether it should be synchronous (RPC) or asynchronous (event), and reason about what would happen with the alternative:
ordersneeds to know whether there is stock before confirming an order.catalogwants to show "only a few left" when the stock drops below 5.- Anna's app wants to show the payment confirmation immediately after she presses "pay".
analyticswants to count every sale for the campaign report.deliveryneeds to know the delivery address of every paid order in order to plan routes.
Exercise 3: Graceful degradation
For each of the following failures, design what must keep working at Kilometre Zero and what message or behaviour the user will see. State which design principle makes it possible:
paymentsgoes down (the external gateway is not responding).- Redis (the catalog cache) goes down.
- The event bus (Kafka) goes down for 10 minutes.
deliverygoes down completely.
Solutions
Solution 1:
(a) Reviews: a good candidate for a new service (reviews). It has its own data (rating, comment, customer, product, date), an independent life cycle (moderation, producer replies), a different load pattern (many reads when viewing a product, few writes) and could be scaled separately. Communication: asynchronous with orders (it consumes OrderDelivered so that only purchased products can be reviewed) and asynchronous towards catalog (it publishes ReviewCreated so that the catalog keeps a denormalised average rating, with no synchronous calls when displaying the product page). The principle: each service owns its data; synchronous only when the answer is needed now (showing the average rating does not require querying reviews on every request).
(b) Coupons: this one is more debatable. The maximum number of uses requires a strongly consistent check at the moment the order is confirmed (coupon use number 101 cannot be allowed if the maximum is 100), which brings it close to the stock problem. Two reasonable options: include it in orders (which already handles the total and the confirmation) or create promotions as a service of its own with a synchronous reserve_use(coupon) call from orders, analogous to the stock reservation. A separate service is justified if campaigns are going to grow in complexity (rules, segmentation, a marketing team with its own deployment pace); if they are just simple coupons, putting them in orders avoids a network boundary in the most critical flow. The important thing is to recognise that the answer depends on the symptom you want to solve, not on a fixed rule.
Solution 2:
- Synchronous. The customer is waiting for the confirmation; without an immediate answer no decision can be made. With events,
orderswould have to wait "a while" forinventoryto reply through another channel, and could not give the customer an answer within the same request. - Asynchronous.
catalogconsumesStockUpdatedand keeps a local indicator. With synchronous calls, every product view would make a call toinventory, multiplying the load and adding a dependency to the highest-volume flow (symptom 1). - Synchronous for the
orders→paymentsinteraction (the customer is waiting), but with a short timeout and a degradation plan: ifpaymentsdoes not respond within 3 seconds, the order is left as "payment pending" and resolved later through events (PaymentConfirmed), with the customer being notified. It is a mixture: synchronous on the happy path, asynchronous as a safety net. - Asynchronous.
analyticsconsumesOrderPaid. A synchronous call toanalyticsfromorderswould couple the purchase flow to an analytical system that does not need to be available in order to sell (symptom 5). - Asynchronous.
deliveryconsumesOrderPaid, which includes the address. Planning routes is not immediate; and ifdeliveryis down, the event waits on the bus and is processed later.
Solution 3:
paymentsgoes down. Catalog, basket, delivery tracking and analytics keep working. When confirming an order, after the timeout, the customer sees: "We couldn't process your payment just now; your order has been saved and we'll let you know as soon as it goes through" (order in the "payment pending" state; the stock is reserved with an expiry). Principle: failure isolation + timeouts + asynchrony as a safety net. Compare this with symptom 2, where this very failure brought the whole platform down.- Redis goes down.
catalogkeeps working by reading from its PostgreSQL, more slowly and with less capacity; during a campaign it might be necessary to limit traffic. The user sees no error, just more latency. Principle: the cache is an optimisation, not a source of truth; the service must work without it. - Kafka goes down for 10 minutes. Synchronous operations (buying, paying, reserving stock) keep working. Events pile up in each producing service (the outbox pattern, lesson 02-05) and are published when the bus comes back. Visible effects: the courier map freezes, the "only a few left" indicator lags, the campaign reports run 10 minutes behind. Principle: synchronous only when needed; asynchronous communication tolerates delays by definition.
deliverygoes down. People can buy and pay as normal. Tracking on the map shows "Tracking temporarily unavailable; your order is confirmed". Courier positions pile up on the bus and are processed on recovery. TheOrderPaidevents wait; routes are planned late. Principle: failure isolation and temporal decoupling through events.
Conclusion
This lesson has turned the foundations of the module into a plan. We have opened up the Kilometre Zero monolith (six modules, one database, a transaction that stretches as far as the external payment gateway, a Tuesday-and-Thursday deployment) and identified the five symptoms forcing its evolution: campaign spikes, the payments failure that brings everything down, teams treading on each other's toes when deploying, real-time courier telemetry and analytics choking production. Each symptom justifies one piece of the target architecture: six services that own their data, synchronous communication over gRPC where an immediate answer is needed and asynchronous communication through Kafka events for everything else, storage tailored to each workload (replicated PostgreSQL, Cassandra, Redis, object storage), batch and stream processing for analytics, and security, observability and Kubernetes deployment as cross-cutting foundations. The table in section 5 is the map linking each component to the lesson that develops it, and the km0/ project with its minimal docker-compose.yml is the starting point we will build on. None of this has been implemented yet: it is a roadmap, and the path will be gradual, extracting one service at a time. The first step is getting two processes to talk to each other reliably, and that is exactly what begins in Module 2, Communication in Distributed Systems, with the network protocols on which everything else rests.
Distributed Architectures Course
Module 1: Introduction to Distributed Systems
- Basic Concepts of Distributed Systems
- Distributed System Models
- Advantages and Challenges of Distributed Systems
- The Fallacies of Distributed Computing
- Time, Clocks and Event Ordering
- From Monolith to Distributed Platform: the Kilometre Zero Case
Module 2: Communication in Distributed Systems
- Communication Protocols
- RPC and RMI
- gRPC and Data Serialization
- Messaging and Message Queues
- Asynchronous Communication Patterns
Module 3: Consistency and Replication
- Consistency Models
- The CAP Theorem and PACELC
- Consensus Algorithms
- Data Replication
- Distributed Transactions and Sagas
Module 4: Distributed Storage
- Data Partitioning and Consistent Hashing
- Distributed File Systems
- Object Storage
- Distributed Databases
- Distributed Caches
Module 5: Distributed Computing
- Distributed Computing Models
- MapReduce and Hadoop
- Spark and In-Memory Computing
- Stream Processing
- Job Scheduling and Data Pipelines
Module 6: Security in Distributed Systems
- Authentication and Authorization
- Encryption and Data Protection
- Identity Management
- Service-to-Service Security: mTLS and Secrets Management
- API Gateways, Rate Limiting and Auditing
Module 7: Monitoring and Maintenance
- Monitoring Distributed Systems
- Centralized Logs and Distributed Tracing
- Failure Management and Recovery
- Resilience Patterns: Timeouts, Retries and Circuit Breakers
- Automation and Orchestration
- Testing Distributed Systems and Chaos Engineering
