Chapter 26 of 27 · Part 12: Hands-on: FlixTube

Building your first microservices

Chapters 1 to 5 of Bootstrapping Microservices as a hands-on path: design ideas, a first HTTP service, Docker, Docker Compose, storage, a database and messaging.

key idea / coordinatorservice / good pathin progress / watch outproblem / failureneutral

Why microservices: managing complexity

Every application grows complex. Microservices do not remove complexity. They cut it into small parts with hard boundaries, so the part you work on every day stays simple.

Tangled monolith parts versus microservices with hard boundariesMonolith: parts blur togetherUploadSearchHistoryBillingMicroservices: hard boundariesUploadSearchHistoryBillingBoth can be split into parts. Process boundaries keep microservice parts from tangling.

A microservice is a small, independent process that runs on its own deployment schedule and can be updated without touching the others. One developer or one small team can own it.

Anatomy of one microserviceCustomerbrowser or appMicroservicesmall, own processOther servicescalled over networkDatabase / storageMany services are internal only. Typically each one has its own state attached.

A microservices application is a distributed program: many small services collaborating. Some are exposed (a gateway for web or mobile users), most are not. An orchestration platform (Kubernetes in the book) runs them as a cluster, on a cloud vendor.

A microservices application in a clusterCloud vendor (Azure in the book)Kubernetes clusterGatewayexposedService AService BService CService DDatabase servermay be outsideFile storagecloud storageOnly the gateway faces users. Kubernetes starts, restarts and connects the services.
TermPlain meaning
Microservicesmall independent process, own deployment schedule, one owner
Microservices applicationdistributed program built from collaborating services
Clustergroup of computers presented as one pool of computing power
Orchestrationautomated management of services (Kubernetes does it here)

From Bootstrapping Microservices, ch. 1 (sections 1.4 to 1.6).

Design principles and how big a service should be

Designing microservices is ordinary software design. The book keeps to a few rules: do not over-design or future-proof, keep refactoring so the design stays simple, and let a good design emerge. Plan every iteration, but expect the plan to change.

Four principles for microservicesSingleresponsibilitySeparationof concernsLoose couplingfew, minimal linksHigh cohesionbelongs togetherLoose coupling lets you upgrade one service without ripples.

Domain-driven design (DDD) answers "what goes in each service". A bounded context is a bubble where all terms and logic fit together, and it maps well to one microservice. A concept like Video can appear in two contexts with slightly different meanings.

Bounded contexts map to microservice boundariesRecommendationsVideo storageUserVideoLikeVideoFilesimilar ideaEach box is a bounded context. The same concept may live in several services.

DRY (do not repeat yourself) bends here. The cost of duplicated code is often lower than the cost of a bad shared abstraction, and sharing code across process and team boundaries is hard. Share when it truly makes sense.

How much should one microservice holdToo smallmore chatterRight-sizedfollows a domain conceptToo bigdrifts toward a monolithThe smaller the services, the more they must talk, and the more complex the whole system gets.
No right answer: the author even dislikes the name "microservices" because it pushes people to go too small. Ask the same question you ask about a function or class: what is one cohesive area of responsibility?

From Bootstrapping Microservices, ch. 1 (section 1.14).

The example app, the way of working, the tools

The running example is FlixTube, a small video-streaming product (video streaming is a classic microservices use case). It grows from one service to a full application over the book.

FlixTube services at a glanceBrowserGatewayUI + RESTVideo streamingStorageCloudUploadHistoryviewed videosMetadataDashed: indirect messaging. Solid: direct HTTP calls.
The path of the bookCh 2Node.js serviceCh 3Docker imageCh 4Compose, dataCh 5messagingCh 6-8K8s, Terraform, CDCh 9automated testsCh 10-11FlixTube, healthCh 12scalingThis lesson covers the first row. Lesson 27 covers the chapters 10 to 12.

The development philosophy is simple: iterate in small steps, keep it working after every step, and move from simple to complex. If a step goes wrong, revert to the last committed state and lose almost nothing.

The personal iteration loopSmall changea few linesTest itsee it workCommitsave to GitRepeatfeedback each loopMany working iterations add up to a large body of working code, with no big-bang risk.
ToolWhy it is here
Node.jsnetwork oriented, light, quick to bootstrap an HTTP server; huge community; JavaScript runs on front end and back end
Gitcommit each working iteration so you can wind back
VS Codeeditor with terminal; later also used to browse logs and shell scripts
Docker, Compose, Kubernetesintroduced in later sections of this lesson and in lesson 19

The choice of Node.js and Azure is only an example. Docker, Kubernetes and Terraform exist precisely so you can swap the language and the cloud vendor.

From Bootstrapping Microservices, ch. 1 (section 1.15) and ch. 2 (sections 2.3 to 2.5).

The first microservice: HTTP video streaming

Chapter 2 builds one Express web server with a single route that streams a video file to the browser. The file is read as a stream and piped to the response, so the server never loads the whole video into memory.

The streaming pathBrowserGET /videoExpress routehandler runsRead streamvideo fileHTTP responsepiped to clientNode.js streams carry the bytes through a pipe. Playback starts before the file ends.

Configuration comes from environment variables, the standard way and supported by all the tools used later. The service reads PORT and refuses to start if it is missing, so a forgotten setting fails loudly, not silently.

Configuring the service with an environment variableEnvironmentPORT=3000Microservicereads process.env.PORTListens on portThrows, will not startsetmissingCompose and Kubernetes later supply the same variables, so one image runs anywhere.

Two modes follow from this chapter and are reused in every later one. Production mode installs only runtime dependencies and starts with the standard npm start. Development mode adds nodemon, which restarts the service on every code change.

Production mode and development modeProduction modeInstall runtime deps onlynpm startDevelopment modeInstall dev deps (nodemon)npm run start:devSame convention in every project, so you never need to remember the main file name.

From Bootstrapping Microservices, ch. 2 (sections 2.6.1 to 2.6.8).

Docker: container, image, Dockerfile, registry

A container virtualizes both the operating system and the hardware, so it is lighter than a virtual machine that carries a full guest OS. That is why many containers fit on one computer and microservices stay affordable.

Virtual machines compared with containersVirtual machinesContainersHardware and host OSVM 1guest OS + serviceVM 2guest OS + serviceHardware and host OSContainerserviceContainerserviceContainerserviceHeavy copies of an OS on the left, small isolated processes on the right.

An image is a snapshot (code, dependencies, assets) and a container is a running instance of it. A Dockerfile is the script that builds the image. A registry stores images so any machine with credentials can pull them.

Build, publish, runDockerfile+ Node.js projectImagedocker buildRegistrydocker pushContainerdocker runThe book publishes to a private registry in the cloud, as a company with proprietary code would.
Reading a DockerfileFROMstart from a base image (a Node.js version)COPYcopy package files, then the source code and assetsRUNinstall dependencies at build timeCMDwhat to run when a container starts (npm start)Each line adds a layer to the image. A .dockerignore file keeps junk out of the build.
CommandJob
docker buildpackage the project as an image (the -t option names it)
docker image listlist local images with tag and size
docker runstart a container from an image, mapping ports and setting variables
docker tag, login, pushtag, authenticate and upload the image to the registry
docker container listshow running containers
A universal package manager: the author likes to think of Docker as one package format for every tech stack, and as a way to give all developers the same environment as production.

From Bootstrapping Microservices, ch. 3 (sections 3.3 to 3.10).

Docker Compose for development

Starting and stopping many containers by hand wastes time. The Docker Compose file is a script that describes the whole application as a set of containers. One command builds and starts them; another stops and removes them.

One file, one command, many containersCompose fileone YAML filedocker compose up --buildService 1Service 2DatabaseContainers reach each other by name: the container name is the host name.

Compose also gives you what a lone service lacks: a reproducible way to boot the whole app, drop one project and start another, and keep several configurations for different slices of the app. Wrap the long commands in small shell scripts.

Docker ComposeKubernetes
Best atbooting the whole app on a laptoprunning the app in production on a cluster
Start-upone file, one commandmany commands, many configuration files
Weightlightheavy; a local copy eats laptop resources
Scales toone machinemany machines
Why not Compose in productionCloud VMyou create itInstall Dockerby handCopy the codeby handRun Composehard to scaleCompose in production means all of this. Kubernetes automates it, so production uses Kubernetes.

From Bootstrapping Microservices, ch. 4 (section 4.3).

Adding storage and a database

Files go in cloud storage (Azure Storage in the book, but S3 or Google Cloud Storage would do the same job). The video-streaming service does not talk to it directly. A small video-storage microservice hides the provider, following single responsibility. Swapping providers later disturbs only that one service.

Hiding the storage provider behind a microserviceBrowserVideo streamingforwards requestVideo storageREST APICloud storageprivate filesThe streaming service forwards its HTTP request and streams the answer back to the client.

The same question applies to where the bytes live. The author prefers a stateless cluster: files and data sit outside Kubernetes, in cloud storage and a managed database. The cluster can then be destroyed and rebuilt at will without data loss, which also enables blue-green deployments (lesson 27).

Cloud storage versus cluster storageStateless cluster (preferred)ServicesRebuild freelyData outside: storage + managed DBState inside the clusterServicesData at riskVolumes inside the clusterCloud storage is also simple, works in development, is cheap and managed for you.

For records, a MongoDB server starts in Compose from a public image, so a database exists in seconds. Services find it by host name and database name, both given as environment variables. In production the book recommends an external, managed database.

Database per microservice, not a shared databaseOne database servervideo-streamingdatabasehistorydatabaseVideo streamingHistoryShared databaseservices coupled through dataFragile, hard to scaleOne server may host many databases. One database per service keeps data private to its owner.
Per microservice or per application? Give each service its own database (a server can host several). A shared database is an integration point that makes the system more fragile and less scalable. Break the rule only for a good, deliberate reason.

From Bootstrapping Microservices, ch. 4 (sections 4.4 and 4.5).

The history service and live reload in Compose

The new history microservice records which videos a user watched. It receives a "viewed" message from the streaming service and writes it to its own database. It is the example used to explore messaging, and later it can feed recommendations.

The viewed messageVideo streaminguser watches"Viewed" messagesent to historyHistoryrecords itHistory DBown databaseFor this chapter the storage service is dropped to keep the focus on communication.

Now the single Dockerfile splits in two. Dockerfile-dev favors fast iteration; Dockerfile-prod favors performance and security. Keep them side by side so the dev one does not drift far ahead.

Two Dockerfiles for two purposesDockerfile-devNo code baked inDev deps, nodemonnpm run start:devDockerfile-prodCode copied into imageProduction deps onlynpm startDev: source is shared from the host. Prod: everything is inside the image.
The live reload loop in ComposeEdit in VS Codesave a fileShared volumesrc in containernodemonsees the changeService restartsrefresh browserUnder WSL2 or a VM, nodemon needs its polling option, because file watching does not cross over.

Production mode should not be left for deployment day. Keep a second Compose file for it and run it now and then, for example with the -f option, so problems do not pile up unseen. Test dev mode every few minutes, production mode every few hours.

From Bootstrapping Microservices, ch. 5 (sections 5.4 and 5.5).

Direct and indirect messaging

Services must cooperate, and the book names two styles. It prefers direct and indirect over "synchronous" and "asynchronous", because those words already mean something else to programmers.

Direct messaging with HTTPSendervideo streamingReceiverhistoryHTTP POST "viewed"direct responseDirect: the receiver is named, cannot ignore the message, and answers at once.

Direct messages over HTTP are easy: HTTP and DNS are everywhere, and in Compose the container name is the host name. One controller can orchestrate a strict sequence, sending a message, waiting for the reply, then sending the next. The price is tight coupling.

Indirect messaging through a brokerSenderBrokerRabbitMQReceiverIndirect: neither side knows the other, so there is no direct reply.
Direct (HTTP)Indirect (RabbitMQ)
Targeta named servicea queue or exchange
Replyimmediate responsenone; sender may not know a receiver exists
Couplingtightloose
Controlone service can orchestratebehavior emerges from the services
Good forstrict sequences, commandsnotifications and evolvable designs

From Bootstrapping Microservices, ch. 5 (sections 5.6 and 5.7).

Indirect messaging with RabbitMQ

RabbitMQ is a mature broker built on AMQP, started in Compose from a public image. Services use the amqplib library. Messages are published to a queue or an exchange. The sender resolves the broker by DNS and never meets the receiver.

Single-recipient messagesVideo streamingViewed queueone shared queueHistory AHistory BSingle recipient: each message goes to exactly one consumer, like a pool of workers.

A service "asserts" a queue, meaning it creates it only if it does not exist yet, so many services can share it. Consumers acknowledge each message once handled.

Multiple-recipient messages with an exchangeVideo streamingViewed exchangefanoutQueue 1Queue 2HistoryRecommendMultiple recipients: each service owns an anonymous queue bound to the shared exchange.

Add a new consumer and the sender never changes. That is the benefit and the risk. With no central controller, behavior emerges from the interplay of messages. It is harder to read, but there is no single point of failure orchestrating everything.

Emergent behaviorService Apublishes eventBrokerdelivers copiesService Breacts, emits moreService Creacts tooNo one service is in charge. Each decides how to react to what it hears.
The start-up order problemCompose starts bothhistory and RabbitMQHistory connects earlybroker not readyWait for the porta start-up delayThe MongoDB library retries on its own. Chapter 11 solves this properly with Kubernetes probes.

From Bootstrapping Microservices, ch. 5 (section 5.8).

Recap

Quick check

Your two services share one database table because it is "easier". What goes wrong?
Show answer

The database becomes an integration point. The services are coupled through data, can no longer change independently, and the shared database becomes a bottleneck. Give each its own database, even if on one server.

You publish one "viewed" event and want both history and recommendations to receive it. Queue or exchange?
Show answer

An exchange bound to one anonymous queue per consumer. A single shared queue would give each message to only one of them.

Why keep a stateless cluster?
Show answer

Data lives outside, so the cluster can be destroyed and rebuilt freely, which also makes blue-green upgrades possible.