7. Chapter review

You've containerised the news aggregator, orchestrated it with Compose, and put Redis in front for caching. This page summarises the patterns and locks them in with a quiz.

You started this chapter with a News Aggregator API that worked perfectly on your laptop but required manual setup for teammates: install PostgreSQL, configure environment variables, run migrations, and start the server. The "works on my machine" problem made sharing your application tedious and deployment risky.

You containerised it with Docker, putting code, runtime, and system dependencies into a single image that runs identically across machines. A multi-stage Dockerfile cut the runtime image down to roughly 187 MB on the worked example (from 452 MB on the naive version), and ordered layers so a code change doesn't reinstall every Python package.

You orchestrated the FastAPI app, Postgres, and Redis as one Compose stack: three services, one YAML, one command. Health checks gate startup order, named volumes survive container rebuilds, and secrets travel via .env rather than being baked into the YAML.

You profiled before caching, recorded the baseline (12 req/sec, ~723ms average response time on the worked example), then put Redis in front of the slow path. The cached endpoint returns in single-digit milliseconds; the load test reflects an order-of-magnitude jump in throughput because most requests no longer wait on NewsAPI or the Guardian.

The same Compose-described shape becomes the starting point for cloud deployment in Chapter 28: a teammate can docker compose up today, and the same image goes to AWS ECS tomorrow.

Patterns to take forward

  • Profile before you cache. Timing middleware plus a small local load test gives you a baseline number per endpoint. Without it, you can't tell whether a cache helped or whether you cached a fast endpoint that didn't need it. The discipline is "measure, then optimise", not the reverse.
  • Image versus container, blueprint versus running thing. The class-and-instance analogy carries everywhere container tooling does: the Dockerfile is source, the image is the artefact, the container is the running process. You'll see the same distinction on every platform that runs containers, from ECS to Kubernetes.
  • Multi-stage builds keep the build toolchain out of the runtime image. Compilers, system libraries, and dev dependencies live in the builder stage; only the artefacts cross the boundary into the runtime stage. The image gets noticeably smaller on this chapter's worked example, but the load-bearing win is that the runtime image stops carrying things that could later be exploited or just take up space.
  • Compose as the dev environment, not just a dev shortcut. One YAML, one command, one network. The same shape (services, env, volumes) is what cloud orchestrators consume, so the time spent on the Compose file is reusable rather than throwaway.
  • Cache-aside with TTL, decorator-shaped. The @cached decorator pattern keeps cache logic out of the route body; the route stays about the request, the decorator handles the lookup-then-fallback. Picking the TTL is the only real decision per endpoint: how stale is too stale for this data.
  • Config from env, not from YAML. Secrets travel via .env (gitignored); a checked-in .env.example documents what the deploy needs without leaking values. Production swaps the source (AWS Secrets Manager, Kubernetes Secrets, etc.) but keeps the same boundary: configuration comes from outside the artefact.

Quiz

Use this quiz to check your understanding of the entire chapter. Try to answer each question out loud or in a notebook before expanding the explanation. If you get stuck, that's a signal to revisit the relevant section.

Select each question to reveal a detailed answer:
Why containerise applications instead of sharing code repositories with setup instructions?

Answer: Containers eliminate environmental differences that cause the "works on my machine" problem. Setup instructions depend on users having correct Python versions, system libraries, and dependencies installed. Any difference creates failures.

Containers package the application with its exact environment (Python version, libraries, configuration). Users run one command, and everything works identically on Mac, Linux, Windows, or cloud platforms. This eliminates hours of debugging environment mismatches and makes deployment reliable.

Sharing repositories with instructions scales poorly. Every developer spends time configuring environments. Containers let developers focus on writing code instead of fixing setup issues.

Explain the difference between containers and virtual machines.

Answer: VMs package complete operating systems (Ubuntu, kernel, system services). Running three applications in VMs means three operating systems consuming gigabytes of memory. VMs take minutes to boot and are heavyweight.

Containers package applications and dependencies, sharing the host OS kernel. Running three applications in containers means one kernel and three isolated environments. Containers are megabytes, not gigabytes, and start in seconds.

Trade-offs: VMs provide stronger isolation (separate kernels) but higher overhead. Containers provide sufficient isolation for most applications with minimal resource usage. For application deployment, containers are the industry standard.

How does layer caching in Docker improve build times?

Answer: Each Dockerfile instruction creates a layer. Docker caches layers and reuses them if instructions and inputs haven't changed. When a layer changes, Docker invalidates that layer and all subsequent layers, rebuilding from that point.

Good structure: Copy requirements.txt first, install dependencies, then copy application code. Changing code invalidates only the final layer. Dependencies stay cached.

Bad structure: Copy all files first, then install dependencies. Changing any file invalidates the dependency installation layer, reinstalling everything unnecessarily.

Order instructions from least-changed to most-changed: base image -> dependencies -> application code. This maximizes cache reuse and reduces build times from minutes to seconds.

What problems does Docker Compose solve that running containers manually doesn't?

Answer: Without Compose, multi-container applications require multiple terminals, complex docker run commands with networking flags, manual startup coordination, and separate stop commands. Documentation is needed for every flag and environment variable.

Docker Compose provides:

  • Declarative configuration: All services defined in one YAML file
  • One-command startup: docker compose up starts everything
  • Automatic networking: Services communicate by name
  • Startup order: Health checks ensure dependencies are ready
  • Version control: Configuration is documented and portable

Teammates clone the repository, run one command, and the full stack works immediately.

Why use named volumes instead of storing data directly in containers?

Answer: Containers are ephemeral. When you stop a container, data stored inside is lost. When you rebuild an image, any data in the old container disappears.

Named volumes provide persistent storage outside containers. Database files live in volumes that survive container restarts, rebuilds, and deletions. docker compose down stops containers but preserves volume data. Starting containers again mounts the same volumes with existing data intact.

Without volumes, every restart means fresh databases with no data. With volumes, your development database persists across restarts like production databases do.

When is caching appropriate and when should you avoid it?

Cache when: Data changes infrequently but is accessed frequently. Retrieval is expensive (external APIs, complex queries). Slight staleness is acceptable (news, product catalogs, weather).

Don't cache when: Data must be absolutely current (bank balances, inventory, auth tokens). Staleness causes incorrect behaviour or security issues. Retrieval is already cheap enough that the cache adds more complexity than value.

The decision framework: "If this data is X minutes old, does it cause problems?" If yes, don't cache. If no, caching likely improves performance dramatically without harming user experience.

Where does Redis's speed advantage actually come from?

Answer: Redis serves reads from RAM rather than disk, which is roughly two orders of magnitude faster than a database round-trip that has to seek, parse, and return.

For this chapter's API, the cached path skips two external API calls (NewsAPI and the Guardian) plus the article-normalisation work plus the DB write. The cache-hit path is a single Redis GET; the cache-miss path is everything the uncached endpoint did before, just once per cache window instead of once per request.

The throughput jump on the worked example (12 to 419 req/sec) isn't really about Redis being fast in isolation; it's about most requests no longer waiting on the slow external dependencies at all.

Why profile performance before containerising instead of after?

Answer: Containers package your code as-is. If you containerise slow code, you get slow containers running on expensive servers. The container doesn't fix performance issues. It preserves them.

Profiling first identifies bottlenecks (external API calls, missing database indexes, inefficient queries). You fix these issues in your local environment where debugging is easy. Then you containerise the optimised version.

Optimising after containerisation means a rebuild-push-redeploy cycle for every fix. Optimising first means containerising once against a known baseline; you can compare image-to-image performance later without re-litigating whether the underlying code was efficient.

The expensive failure mode this prevents is scaling horizontally (adding servers) when the actual problem is a missing index or an N+1 query, something a small code change would have fixed for free.

Looking ahead

The stack runs on your laptop now. The next two chapters move it off the laptop and onto infrastructure that doesn't sleep when you close the lid.

Chapter 28 takes the same image you built here and pushes it to AWS: ECR for the image registry, ECS Fargate for the runtime, RDS and ElastiCache instead of the local Postgres and Redis containers, an Application Load Balancer in front. The Compose file's service shape (api, db, cache) maps almost directly onto the cloud equivalents, which is why this chapter is structured the way it is: the local stack is a rehearsal for the production one.

Chapter 29 then fills in the operations layer that turns "deployed" into "operable": a CI/CD pipeline so git push tests, builds, and deploys; CloudWatch logs and alarms so you find out about regressions before users do; auto-scaling so the cluster grows under load and shrinks at night.

Next, in Chapter 28, the image you built here gets pushed to ECR and run on ECS: same Dockerfile, same Compose shape, different runtime.