Microservices Introduction
How to actually split a monolith, what a real microservice codebase and deployment pipeline look like.
What's a microservice, again?
A microservice is an independently deployable service that owns its own data and talks to other services over the network rather than through in-process function calls. The architectural trade-offs — why you'd want that, what it costs you, API gateways, circuit breakers, the Saga pattern — are covered in depth on the System Design track's Microservices vs Monolith page. Read that first if you haven't; this page (and the rest of this track) assumes you already know why you might split a system up, and focuses entirely on how you actually do it.
Splitting a monolith: where do the seams go?
The single most common mistake in a first migration is picking the wrong seams. Splitting along technical layers — a "UI service," a "business logic service," a "database service" — produces services that must call each other constantly for anything to work at all. That's not microservices, it's a monolith with extra network hops (often called a distributed monolith), and it's strictly worse than the monolith you started with.
Split along business capability instead (this is the essence of Domain-Driven Design's "bounded context"). Good candidates for a first extraction share these traits:
- A narrow, well-defined interface. If describing what the module does takes one sentence ("reserves and releases inventory"), it's a good candidate. If it takes a paragraph full of "and also," it isn't — yet.
- A different scaling profile. An image-processing or notification-sending module often needs to scale independently of the rest of the app.
- A different release cadence or owner. A module a specific team owns end-to-end, and wants to deploy on its own schedule, is a natural boundary.
- Low, well-understood coupling to everything else. Check how many other modules call into it, and how many database tables it touches that nothing else touches.
Classic first-extraction candidates in most systems: notifications/email, search or indexing, file/image/video processing, authentication. Classic bad first candidates: anything still deeply entangled with the core domain model (in an e-commerce app, that's usually Orders — leave it for last, once the team has practice).
Migrating without a big-bang rewrite: the Strangler Fig pattern
Rewriting a module from scratch and cutting over all at once is high-risk — you find out everything you missed on launch day, under load, in production. The Strangler Fig pattern (named after the vine that gradually envelops and eventually replaces its host tree) migrates incrementally instead:
1. Put a routing facade in front of the monolith.
Client --> Facade --> Monolith (handles everything, as before)
2. Stand up the new service. Redirect ONE route/behavior to it at a time.
Client --> Facade --> Monolith (most routes)
`--> New Inventory Service (only /inventory/* so far)
3. Keep redirecting routes until the monolith's inventory code
handles nothing. Delete it.
Client --> Facade --> Monolith (inventory code gone)
`--> New Inventory Service (all inventory routes)
At every step the system is fully working — you can pause, roll a single route back, or ship the migration over months instead of betting everything on one cutover weekend.
What a real microservice's codebase looks like
A microservice is small, but it isn't a script — it still needs its own build, its own deployment pipeline, and its own contract for other teams to code against:
inventory-service/
├── src/
│ ├── main/java/com/noalabs/inventory/
│ │ ├── api/ # REST controllers (or gRPC handlers)
│ │ ├── domain/ # business logic, entities, invariants
│ │ ├── repository/ # data access — this service's DB only
│ │ └── messaging/ # Kafka/RabbitMQ producers & consumers
│ └── resources/
│ └── application.yml
├── Dockerfile
├── docker-compose.yml # local dev: this service + its own DB, nothing else
├── .github/workflows/ci.yml # its own independent build & test pipeline
└── openapi.yaml # the public contract other teams code against
The detail that matters most: the database directory doesn't exist here because there isn't one to share — inventory-service owns its schema exclusively, and no other service is allowed to query it directly. The only way in is this service's API, or events it publishes. That rule is what actually gives you independent deployability; a shared database silently undoes it (see Common mistakes below).
Deployment: one service, one pipeline
Each service builds and deploys as its own versioned artifact — a container image tagged something like inventory-service:1.4.2 — with a pipeline that can run and finish without touching any other service's pipeline. Deploying a new version of inventory-service should never require a coordinated, same-day redeploy of orders-service.
That independence isn't free: since you can't force every caller to upgrade in lockstep with you, a service has to keep its previous API contract working for some deprecation window (commonly via URL versioning like /v1/reservations and /v2/reservations running side by side, or additive-only changes to a stable contract). Skipping this — changing a response shape or removing a field and expecting every consumer to have already updated — is how a "safe, independent" deploy turns into a cross-team incident.
A minimal example: extracting an Inventory service
Before (inside the monolith): OrdersController calls InventoryModule.reserveStock(orderId, items) as a plain in-process method call, inside the same database transaction that creates the order. If it fails, the whole transaction rolls back — trivially consistent.
After (Inventory extracted): OrdersService calls POST /inventory/reservations over HTTP against the new InventoryService, running as a separate process with its own database. There is no shared transaction anymore — a failure partway through now needs an explicit compensating action instead of an automatic rollback (the Saga pattern, covered on the System Design track). The very next page in this track, Service Communication, shows exactly what that call looks like in code, both as a direct synchronous request and as an alternative event-driven design.
Common mistakes
- Splitting along technical layers instead of business capability — the result is a distributed monolith where nothing works without three other services also being up.
- Sharing one database across "independent" services "just for now" — this silently recreates monolith-style coupling (any service's migration can break another) without any of the monolith's simplicity.
- Attempting a big-bang cutover instead of the Strangler Fig pattern, betting an entire migration on one high-risk release.
- Treating "independently deployable" as just "has its own git repo" — without API versioning and backward compatibility, deploys still require cross-team coordination.