Decomposing the Application
Decomposing the Application
A capstone starts with a diagram, not YAML. Take a typical web app and split it into pieces that can fail and scale independently: a web frontend (nginx serving static assets), an API (your language runtime), a background worker consuming jobs, PostgreSQL for state, and Redis for cache or queues.
For every edge, write two contracts: the protocol (HTTP across the compose network, TCP on 5432 for Postgres) and who owns the data. Services must be stateless — persist to Postgres or Redis, never to the container filesystem, or a restarted container loses work. Put everything on one user-defined network so Compose DNS resolves api and db by name instead of hardcoded IPs that change on every up.
Compose Service Topology
Compose Service Topology
services:
web:
image: nginx:1.27
ports: ["8080:80"]
depends_on:
api: { condition: service_healthy }
api:
build: ./api
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
worker:
build: ./worker
environment: { REDIS_URL: redis://cache:6379 }
db:
image: postgres:16
volumes: [dbdata:/var/lib/postgresql/data]
volumes:
dbdata: {}
Only web publishes a host port; db stays reachable solely on the internal network. The named volume dbdata preserves Postgres data across docker compose down, and service_healthy gates startup until the API health check passes.
Architecture Review and Tradeoffs
Architecture Review and Tradeoffs
Run the design through this checklist: does any service write to its own container layer (it should not)? Does scaling the API require redeploying the worker? Is Postgres accidentally published on the host? Each right answer is a boundary you defended.
Common pitfalls: bundling cron into the app container, which couples their lifecycles; using legacy links: instead of default DNS networking; hardcoding container IPs; and embedding database credentials in the image rather than environment or files.
Remember Compose is single-host. The moment you need multi-host scheduling, the same service definitions carry to Swarm or Kubernetes — that portability is part of the architecture's value.
Interview: justify every service with an independent scaling or failure-domain reason, or merge it.
Quiz
1. Why should services be stateless in this design?
2. What does depends_on with condition: service_healthy guarantee?
3. How does the api container reach the database?
Flashcards
Question
Service boundary test
Click to reveal answer
Answer
A boundary is justified if the service scales or fails independently; otherwise consider merging.
Question
Compose DNS
Click to reveal answer
Answer
Services reach each other by service name over the default project network (e.g., db:5432).
Question
Named volume
Click to reveal answer
Answer
Persists data across docker compose down; container layers do not.
Question
Health-gated startup
Click to reveal answer
Answer
depends_on: {condition: service_healthy} waits for the target healthcheck before starting.
Revision Notes
Key Takeaways
- 1. Design service contracts (protocol + data ownership) before writing compose YAML
- 2. Stateless services plus named volumes survive restarts and scaling
- 3. Only the frontend publishes ports; databases stay on the internal network
Interview Tips
- • For every service, state why it is not merged into another — scaling axis or failure domain
- • Be ready to explain what breaks when Compose moves from one host to an orchestrator
Cheat Sheet
Capstone: web → api → worker + db/redis. Stateless services; named volumes for state; service DNS on one user-defined network; healthcheck + depends_on condition; publish only frontend ports.
Continue Learning
Before You Start
- No prerequisites listed.