FlixTube and running in production
Chapters 10 to 12 of Bootstrapping Microservices: the finished FlixTube app, three ways to deploy it, keeping it healthy and reliable, scaling it, and moving an existing system toward microservices.
key idea / coordinatorservice / good pathin progress / watch outproblem / failureneutral
FlixTube: the finished application
Chapter 10 ships the complete FlixTube. It reuses the services from earlier chapters and adds a gateway and an upload service. A typical microservice has the same layout everywhere, which makes every project feel familiar.
| Service | Purpose |
|---|---|
| Gateway | entry point that serves the web UI and a REST API |
| Video streaming | streams videos from storage to the viewer |
| Azure storage | stores and retrieves videos in external cloud storage |
| History | records the viewing history |
| Metadata | records details about each video |
| Video upload | orchestrates upload of new videos |
| Mock storage | stand-in for Azure storage that uses the local disk |
A monorepo keeps things simple at the start. It once defeated the point of microservices, because everything deployed together. With one GitHub Actions pipeline per subfolder, each service can again deploy on its own schedule.
From Bootstrapping Microservices, ch. 10 (sections 10.4).
Running and testing in development
A service is booted alone with npm install and npm start while you focus on it. The entire app boots with docker compose up --build. Wait for the gateway to print that it is online, then browse to the UI, upload a video, watch it and look at the history page.
Testing reuses lessons 14 and 15. The same word npm test means Jest inside a service and Playwright at the repository root. That convention means you never need to remember which framework a project uses.
| Where | npm test runs | Scope |
|---|---|---|
| metadata folder | Jest unit tests (database library mocked) | one service |
| repository root | Playwright, in three browsers | whole running app |
| npm run test:watch | Jest in live reload mode | while coding |
From Bootstrapping Microservices, ch. 10 (sections 10.5 and 10.6).
Deep dive: fixtures, mocks, gateway, UI, streams
Database fixtures load realistic data before a test, a manual check or a demo. They can be JSON for static data or JavaScript for generated data. A small REST API for loading fixtures runs beside the app during Playwright tests.
Mocking the storage microservice swaps a whole service for a simpler one. Mock storage keeps videos in the local filesystem through a shared Docker volume. The other services cannot tell the difference. Development no longer needs the network or a cloud account, and developers cannot disturb each other.
The gateway is the single door into the cluster. It serves the UI and a REST API and forwards requests inward. Authentication would go here. If web, mobile and admin frontends have different needs, give each its own gateway (backends for frontends) on its own host name.
The UI is deliberately plain: server-rendered pages from Express and Handlebars templates, vanilla JavaScript and Tailwind CSS for styling. The home route fetches the video list from the metadata service and renders it. A real product would probably use a single-page framework, but UI is not the topic of the book.
From Bootstrapping Microservices, ch. 10 (sections 10.7.1 to 10.7.4).
Following a video: streaming passes through three services. Each pipes the stream it receives into its own response, so the video moves byte by byte from cloud storage to the video element in the page.
Video streaming also broadcasts a "video viewed" message, so the history service can record it. A new consumer can be added later without editing the streaming service.
From Bootstrapping Microservices, ch. 10 (sections 10.7.5 and 10.7.6).
Deployment flows: local, manual, automatic
Deployment is practiced in three steps of rising difficulty. If one fails, step back to the easier one. You cannot automate what you cannot do by hand.
Local: a script builds each image and applies each service's YAML to the Kubernetes bundled with Docker Desktop. The gateway appears on a NodePort at or above 30000. No container registry is needed, and a delete script removes it.
Manual production: log in to the private registry, set the registry address as an environment variable, then run the deploy script. It builds and pushes each image, uses envsubst to fill in the templated YAML, and pipes the result to kubectl.
Each workflow is scoped to its service folder, and each can also be started by hand from the GitHub UI. Shell scripts do the work, driven by environment variables. MongoDB and RabbitMQ are deployed once, manually, with an infrastructure script, since they rarely change. For real production the book still advises an external managed database.
From Bootstrapping Microservices, ch. 10 (sections 10.8 to 10.10).
Healthy microservices: seeing what happens
A healthy microservice is not crashing, buggy or starved of CPU and memory. To keep the app healthy you observe it, protect customers when it fails, triage, and debug. Without logs you cannot fix what you cannot see.
| Where | How to read the logs |
|---|---|
| Development, one service | terminal output |
| Docker Compose | one merged stream; pipe it through tee to also save a file |
| Kubernetes | kubectl logs with the pod name; a pod can have several containers |
| A growing cluster | install an aggregator; Kubernetes has none built in |
Log meaningful events and the success or failure of important operations. Do not log secrets, personal details or facts easy to find elsewhere. More logs help more than fewer, because you cannot add logging after the problem has happened.
Observability goes further. Services output rich telemetry (OpenTelemetry is the standard), and a tool such as Honeycomb connects the dots. A trace shows one request crossing services and points to the span that failed.
From Bootstrapping Microservices, ch. 11 (sections 11.1 to 11.2.7).
Error handling, health checks, debugging
Errors are unavoidable: runtime exceptions, bad input, a broken dependency such as RabbitMQ or storage. Choose a strategy. The default recommended here is abort and restart: log the error, exit with a nonzero code, and let the platform restart the process. Use resume only where aborting loses work, such as video uploads in progress.
Kubernetes already restarts crashed pods. For a custom definition of healthy, give each service two routes. The readiness probe says the service is started and can take requests. The liveness probe says it is still alive. Probes also solve the start-up ordering problem from lesson 26, and what happens if RabbitMQ restarts later.
Debugging a distributed app is harder because the problem hides among many small processes. A repeatable process helps:
In production, check the logs first, including those of the previous instance of a crashed pod. List the pods: an image pull error in the status means the cluster cannot reach the registry, usually a connection set-up mistake.
| Symptom | First move |
|---|---|
| Service crashed | kubectl logs for the pod, and the previous instance |
| Pod in an error state or restarting | kubectl get pods to read status and restart count |
| Image pull errors | check the registry connection and image name |
| Cannot find the problem | add experiments in production only if you cannot reproduce it elsewhere |
From Bootstrapping Microservices, ch. 11 (sections 11.2.2, 11.2.8, 11.3).
Reliability: surviving failure
Fault tolerance starts inside each service with defensive programming: expect bad inputs, hidden bugs, future bugs and unreliable dependencies. Defensive testing then attacks the code, checking that errors are reported and recovered from.
Protect data first. Source data captured from customers matters more than data that can be regenerated. Automate backups and practice restoring. Keep the capture code minimal and well tested, and never write code that overwrites or deletes source data.
Replication gives redundancy. Run at least three instances behind a load balancer; when one fails, others take over while Kubernetes restarts it. It is one field in the deployment YAML. Still log and investigate every failure.
Fault isolation is a strength of microservices, if you use it. If video upload breaks, the gateway should catch the error and tell the user that upload is unavailable, so the rest of the app keeps working. This is the bulkhead idea, taken from ship compartments: a leak is contained.
Simple techniques you can use now:
A job queue stores heavy tasks (such as a thumbnail per uploaded video) in a database-backed queue service. Workers pull one job when free, jobs survive a crash and failed jobs are tried again. The load is spread over time, avoiding a burst of extra compute.
From Bootstrapping Microservices, ch. 11 (sections 11.4 to 11.4.7).
Scaling the team: repositories and environments
One developer can push straight from a laptop to customers. A growing team cannot: code should not go directly to customers, developers must not trip over each other, and one repository eventually becomes too much. Microservice boundaries give natural seams for splitting teams, each owning its services from development through production.
A meta-repo (the meta tool and a .meta file) ties the separate repositories back into one virtual repository, so cloning and running everything feels like a monorepo again. A team can keep its own meta-repo for the set of services it works on.
Environments reuse the Terraform from lesson 19, parameterized by an environment name, with Terraform state stored in cloud storage per environment. Pushing to the development branch runs CI tests and deploys to development. The less time between merges, the fewer integration surprises.
Finally, application configuration can move to its own repository that records which version of each service is deployed. Deployment then has two phases: the service pipeline builds and publishes an image, then a change to the config repository deploys it.
From Bootstrapping Microservices, ch. 12 (sections 12.2.1 to 12.2.7).
Scaling performance
Microservices let you tune performance part by part, instead of only scaling a whole monolith. Measure first with the Kubernetes dashboard or cloud portal, then use the lightest option that works. Most of these are one-line changes in Terraform or YAML.
For one overloaded service, raise its replicas. The same field serves redundancy and performance. Elastic scaling adds and removes capacity automatically: the cluster autoscaler grows the node pool between a minimum and a maximum, and a horizontal pod autoscaler grows a service between its own minimum and maximum, so you pay only for what the load needs.
From Bootstrapping Microservices, ch. 12 (sections 12.3 to 12.3.7).
Safe changes and basic security
Many production problems come from pushing broken code. Catch them early, where they are cheaper, with a chain of safeguards.
A rolling update replaces replicas one by one, moving on only when the new replica passes its readiness probe. If it fails, the rollout halts and you can roll back. Settings cap how many extra and unavailable pods are allowed at once.
Use blue-green for risky infrastructure changes, such as the scaling changes above. Build the whole second environment from Terraform, test it, then repoint DNS. A stateless cluster makes this safe.
Security is a book of its own, but the basics matter. FlixTube exposes only the gateway; RabbitMQ and MongoDB were closed to the outside after early experiments. Planned upgrades include authentication at the gateway and HTTPS. Think like an attacker: how would someone break this?
Keep passwords, tokens and API keys out of the code. Use GitHub secrets, Kubernetes secrets, or a tool such as Vault. Simple security has fewer hiding places for flaws.
From Bootstrapping Microservices, ch. 12 (sections 12.4 and 12.5).
Refactoring to microservices and the spectrum
Converting an existing monolith is done like everything else here: small tested steps that keep it working, while features still ship. Before you start, ask whether microservices are truly needed. Then follow the tips.
Carve along natural seams: components that already look like the target services. Without clear seams, expect to refactor first, with tests. Extract first what changes often or would gain most in speed or scale, so benefits appear early.
Returns diminish: the first extractions bring big wins, later ones less, and services that are too small cost more than they give. It is fine to stop. The hybrid approach, a monolith surrounded by a constellation of helper services for jobs like thumbnail generation, is the author's default when unsure.
| Microservices on a budget |
|---|
| Start with a monorepo and one CD pipeline per service; add a meta-repo after splitting |
| One database server, one database per service |
| A one-VM cluster, a single instance per service, simple pipelines |
| External file storage and database so the cluster stays stateless |
| Without automated tests, script a fast boot with Compose and fixtures; invest early in automated deployment |
From Bootstrapping Microservices, ch. 12 (sections 12.6 to 12.9).
Recap
- FlixTube = gateway, streaming, storage, upload, history, metadata, in a monorepo with identical service layouts.
- Mock services and database fixtures make local development and testing quick and independent.
- Deploy first to local Kubernetes, then by hand to production, then through one CD pipeline per service.
- Observe with logs, aggregation and observability; use probes and restarts; debug with a repeatable process.
- Build reliability: defensive coding, data protection, replicas, fault isolation, timeouts, retries, job queues, circuit breakers.
- Scale the team with separate repositories, a meta-repo, environments per branch, optional config repository.
- Scale performance vertically, horizontally and elastically; keep one database per service; do not scale early.
- Reduce risk with branch protection, a test environment, rolling updates, blue-green; start with internal trust and secret handling.
- Convert monoliths in small steps along natural seams; hybrids and partial conversion are legitimate.
Quick check
Show answer
The gateway catches the error from upload (fault isolation), shows a message that upload is unavailable, and keeps serving the other routes. This is graceful degradation.
Show answer
With a rolling update that waits on readiness, the rollout stops after the first replica, the other replicas keep serving, and you debug and roll back.
Show answer
Early on you rarely need redundancy or performance, and it costs little. The design leaves a path to scale by changing replicas and node counts later.