Monoliths and Microservices¶
What You'll Learn¶
- The real trade-offs between a monolith, a modular monolith, and microservices
- What containers make easier — and the distributed-systems problems they leave untouched
- How to containerize a monolith first, then extract a service safely
- The operational capabilities you need before splitting
Three Shapes¶
flowchart LR
subgraph Monolith
M["One deployable<br>UI + auth + orders + payments"] --> MD[(One database)]
end
subgraph "Modular monolith"
MM["One deployable<br>with enforced internal modules"] --> MMD[(One database,<br>schema per module)]
end
subgraph Microservices
S1[auth] --> D1[(auth DB)]
S2[orders] --> D2[(orders DB)]
S3[payments] --> D3[(payments DB)]
S2 -->|HTTP / events| S3
end
| Monolith | Modular monolith | Microservices | |
|---|---|---|---|
| Deploy | Everything together | Everything together | Each service independently |
| Scale | Whole app | Whole app | Per service |
| Calls between parts | In-process function calls | In-process, through module interfaces | Network calls that can fail or be slow |
| Data | Shared database | Shared database, separated schemas | A database per service |
| Debugging | One stack trace | One stack trace | Distributed tracing across services |
| Team fit | One team or a few | Several teams with clear module owners | Many teams owning services end to end |
A monolith isn't a mistake. For most new products, a well-structured monolith ships faster and fails in simpler ways.
What Containers Actually Change¶
Containers make it cheap to package and run many independently versioned processes, each with its own dependencies. That removes a big practical barrier to microservices: you no longer need a server, runtime install, and deployment script per service.
They don't solve any of these:
| Problem | Why it appears with microservices |
|---|---|
| Partial failure | A dependency can be down, slow, or return errors while everything else is fine |
| Latency | A page that made 20 function calls now makes 20 network calls |
| Data consistency | No single transaction spans the orders and payments databases |
| Service discovery and routing | Services must find each other as instances come and go |
| Versioning | API changes must stay compatible with callers deployed on different schedules |
| Observability | One request's story is spread across many services' logs |
| Security | Service-to-service authentication and authorization, network policy |
| Operational load | Many pipelines, dashboards, alerts, and on-call responsibilities |
Start by Containerizing the Monolith¶
Before splitting anything, get the monolith into a container with the practices from this course: a reproducible Dockerfile, configuration from the environment, logs to stdout, a health endpoint, and persistent data in volumes or a managed database.
services:
shop:
image: registry.example.com/shop/monolith:3.12.0
environment:
DATABASE_URL: postgresql://shop:${DB_PASSWORD}@db:5432/shop
ports: ["127.0.0.1:8000:8000"]
depends_on:
db: { condition: service_healthy }
db:
image: postgres:16
environment: { POSTGRES_USER: shop, POSTGRES_PASSWORD: "${DB_PASSWORD}" }
volumes: [db-data:/var/lib/postgresql/data]
healthcheck: { test: ["CMD-SHELL", "pg_isready -U shop"], interval: 5s, retries: 10 }
volumes:
db-data: {}
You get repeatable deployments, horizontal scaling (several identical replicas behind a load balancer), and a foundation for everything else — without taking on distributed-system complexity yet.
Extract a Service With the Strangler Fig Pattern¶
When one part has a genuinely different need — for example, payment processing needs PCI isolation and a separate release cadence — extract it gradually behind the existing entry point:
flowchart LR
U[Users] --> P["Reverse proxy"]
P -->|"/payments/*"| NEW["payments service (new)"]
P -->|"everything else"| MONO["monolith"]
MONO -->|"calls via API, not DB"| NEW
NEW --> PDB[(payments DB)]
MONO --> MDB[(shop DB)]
- Define the boundary inside the monolith first: payments code only reached through one interface.
- Build the new service implementing that interface, with its own data store.
- Route a slice of traffic at the proxy (one endpoint, or a percentage) to the new service.
- Move data ownership: migrate payments data; the monolith stops touching those tables.
- Delete the old code from the monolith once traffic has fully moved.
The proxy routing is the same technique as the Caddy lab.
Design for Partial Failure¶
Network calls need explicit handling that function calls never did:
- Timeouts on every outbound call — never wait forever.
- Retries with backoff and jitter, only for idempotent operations.
- Circuit breakers to stop hammering a failing dependency.
- Idempotency keys so a retried "charge card" request doesn't charge twice.
- Asynchronous events (a message queue) for work that doesn't need an immediate answer.
- Graceful degradation: the product page still renders if recommendations are down.
Readiness Checklist Before Splitting¶
- Automated CI/CD per deployable, with rollback
- Centralized logs with a request or trace ID across services
- Metrics and alerting per service
- Distributed tracing (OpenTelemetry)
- A way to run the services together locally or in a shared environment
- Clear team ownership and on-call for each service
- Service discovery and a routing layer (Compose networks locally; Kubernetes Services and Gateway API in clusters)
If most boxes are empty, a modular monolith is the safer next step.
Common Mistakes¶
- Splitting by technical layer ("database service", "UI service") instead of business capability.
- A "distributed monolith": separate services that must all be deployed together and share one database.
- Adopting microservices because containers make it possible, without a team or scaling reason.
- Synchronous call chains five services deep, where one slow service stalls everything.
- Extracting services before having tracing and centralized logging.
Check Your Understanding¶
- Name three problems microservices introduce that containers don't solve.
- Why is containerizing the monolith first a useful step?
- What does the strangler fig pattern route, and where?
- What makes a system a "distributed monolith"?
Next¶
Continue to Troubleshooting Like a DevOps Engineer.