Overview
Microservices architecture splits an application into small, independently deployable services, each owning a business capability and its own data, communicating over the network via APIs or events. Each service can be developed, deployed, scaled, and even written in a different language by an autonomous team.
The benefits are team autonomy, independent deployments, targeted scaling, and fault isolation. The costs are distributed-systems complexity: network failures, latency, data consistency without shared transactions, observability across many services, and significant platform investment (CI/CD, service discovery, monitoring). Microservices solve organizational scaling problems more than technical ones.
Each stall has its own kitchen, staff, menu, and hours. One stall can expand or close for repairs without affecting others, but coordinating a combined meal from several stalls takes more effort.
When to use it
- Many teams working on one product who block each other.
- Parts of the system with very different scaling or availability needs.
- Well-understood domain boundaries.
- Organizations able to invest in platform and DevOps maturity.
Where it shows up in interviews
Recognize it when: how would you split this system into services?
- Design Uber's backend
- Design Netflix's architecture
- Design an e-commerce platform
Recognize it when: a flow touches several services.
- Design checkout across services
- Design a travel booking flow
Where it is used in real software
Moved from a monolith to hundreds of microservices on AWS, building tools like Hystrix, Eureka, and Zuul.
The 'two-pizza team' model with service ownership; teams build and run their services.
Segment merged over 100 microservices back into a monolith when the overhead outweighed the benefits.
Key terms
- Bounded context
- Domain boundary a service owns.
- Database per service
- No other service accesses its data directly.
- API contract
- The versioned interface other services depend on.
- Distributed monolith
- Services so coupled they must change and deploy together.
- Strangler fig
- Incrementally replacing a monolith by routing features to new services.
How it works, step by step
- 1Find boundaries
Use domain-driven design to identify bounded contexts.
- 2Give each service its data
Own schema; share via APIs or events.
- 3Choose communication
Synchronous for queries, asynchronous events for side effects.
- 4Build the platform
CI/CD, discovery, gateway, observability, resilience.
- 5Handle consistency
Sagas, outbox, idempotency.
E-commerce decomposition
Services and their data
| Service | Owns | Communicates by |
|---|---|---|
| Catalog | Products, prices | REST reads; ProductUpdated events |
| Cart | Carts (Redis/DynamoDB) | REST |
| Orders | Orders, order state | Saga orchestrator; OrderPlaced events |
| Payments | Charges, refunds | Commands and events |
| Inventory | Stock levels, reservations | Reserve/release commands |
| Notifications | Templates, delivery logs | Consumes events |
NOWService: Catalog | Owns: Products, prices | Communicates by: REST reads; ProductUpdated events
Each service can scale and deploy independently; checkout consistency is handled by a saga rather than one transaction.
Implementation
// Orders service: owns its DB, calls inventory synchronously, publishes events asynchronouslyapp.post("/orders", async (req, res) => { const { customerId, items } = req.body; // Synchronous call with timeout: we need the answer now const reservation = await withTimeout(inventoryClient.reserve(items, { idempotencyKey: req.header("Idempotency-Key")! }), 800); if (!reservation.ok) return res.status(409).json({ error: "Out of stock" }); const order = await db.transaction(async (tx) => { const o = await tx.orders.insert({ customerId, items, status: "pending_payment", reservationId: reservation.id }); await tx.outbox.insert({ type: "OrderPlaced", payload: o }); // payments and notifications react asynchronously return o; }); res.status(201).json(order);});Complexity and performance
vs nanoseconds in-process.
Deep chains hurt.
Trade-offs
Teams move independently, but the system becomes distributed: debugging, testing, and consistency are harder.
Scale only what needs it, but each service adds deployment, monitoring, and on-call cost.
Variants and related techniques
Fewer, larger services aligned with teams.
Functions per service or capability.
Common mistakes
- Shared database between services.
Fix: Creates hidden coupling; each service owns its data.
- Long synchronous call chains.
Fix: Latency and failure multiply; use async events and caching.
- Splitting too fine or too early.
Fix: Start with a modular monolith; split along proven boundaries.
Interview questions
What are the main challenges of microservices?
Network failures and latency, data consistency without shared transactions, distributed tracing and debugging, versioning APIs, testing across services, and the operational overhead of deploying and monitoring many services.
How do you split a monolith into microservices?
Identify bounded contexts, modularize first, then extract one capability at a time using the strangler fig pattern: route its traffic to the new service, migrate its data, and remove the old code.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Decompose a food delivery app into services | Medium | Boundaries and data. |
| Plan a strangler migration from a monolith | Hard | Incremental extraction. |