Microservices are one of the most discussed — and most misunderstood — ideas in modern backend engineering. The term gets used for anything from “we split our app into two services” to full-blown platforms with hundreds of independently deployed components. This article walks through what microservices actually are, the building blocks that hold them together, and the reasons you might (or might not) want them.
What is a microservice?
A microservice is a small, independently deployable service that owns a single business capability. Instead of one large codebase that handles orders, payments, catalog, shipping, and notifications together, each of those becomes its own service with its own database, its own deployment pipeline, and its own team.
The defining characteristics are:
- Single responsibility. Each service does one thing — and owns the data for it.
- Independent deployment. You can ship the payment service without redeploying the catalog.
- Process isolation. Services run in their own processes (containers, VMs, serverless functions) and communicate over the network.
- Decentralized data. No shared database. Each service owns its storage.
- Technology freedom. One service can be Go, another Python, another Node — within reason.
Monolith vs. microservices
┌────────────────────────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ MONOLITH │ │ Orders │ │ Catalog │ │ Payments │
│ │ │ │ │ │ │ │
│ ┌──────┐ ┌──────┐ ┌───────┐ │ vs. │ ┌────┐ │ │ ┌────┐ │ │ ┌────┐ │
│ │Orders│ │Catlg │ │Payment│ │ │ │ DB │ │ │ │ DB │ │ │ │ DB │ │
│ └──────┘ └──────┘ └───────┘ │ │ └────┘ │ │ └────┘ │ │ └────┘ │
│ ┌──────────────┐ │ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ Shared DB │ │ │ │ │
│ └──────────────┘ │ └─────────────┴─────────────┘
└────────────────────────────────┘ message bus / HTTPA monolith is a single deployable with a single database. A microservices system is a mesh of small services, each with its own database, communicating over the network.
Why teams adopt microservices
The honest answer: most teams adopt microservices because their organization is growing, not because their software is. The architecture exists to let many teams ship in parallel without tripping over each other.
Concretely, the wins are:
- Team autonomy. Teams own services end-to-end. No cross-team release trains.
- Independent scaling. The product search service can run 50 replicas while the admin panel runs 1.
- Fault isolation. A crash in the recommendations service doesn’t take checkout down — if you designed the boundaries right.
- Technology flexibility. Image processing in Rust, analytics in Python, web API in TypeScript.
- Smaller blast radius. A bad deploy affects one service, not the whole product.
The building blocks
Here’s the anatomy of a realistic microservices system.
┌───────────────────┐
│ Client (Web, │
│ Mobile, Partner) │
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ API Gateway │
│ (auth, routing, │
│ rate limiting) │
└─────────┬─────────┘
│
┌──────────────┬────────────────┼────────────────┬──────────────┐
│ │ │ │ │
┌────▼────┐ ┌─────▼─────┐ ┌──────▼──────┐ ┌─────▼─────┐ ┌────▼─────┐
│ Auth │ │ Orders │ │ Catalog │ │ Payments │ │ Shipping │
│ Service │ │ Service │ │ Service │ │ Service │ │ Service │
└────┬────┘ └─────┬─────┘ └──────┬──────┘ └─────┬─────┘ └────┬─────┘
│ │ │ │ │
┌────▼────┐ ┌─────▼─────┐ ┌──────▼──────┐ ┌─────▼─────┐ ┌────▼─────┐
│ Users │ │ Orders │ │ Products │ │ Payments │ │ Shipments│
│ DB │ │ DB │ │ DB │ │ DB │ │ DB │
└─────────┘ └───────────┘ └─────────────┘ └───────────┘ └──────────┘
┌─────────────────────────────────┐
│ Message Broker (Kafka/NATS) │
└─────────────────────────────────┘
(events: OrderCreated, PaymentSettled, ItemShipped)1. API Gateway
The single entry point for clients. It handles concerns that don’t belong in any individual service: authentication, TLS termination, rate limiting, request routing, response aggregation. Common implementations: Kong, Envoy, AWS API Gateway, Nginx.
Without a gateway, every client has to know every service. With it, clients see one stable interface while services come and go behind it.
2. Service Discovery
Services don’t have fixed addresses — they scale up and down, move between hosts. A service registry (Consul, etcd, Kubernetes DNS) lets services find each other by name instead of IP.
┌──────────┐ "where is payments-service?" ┌──────────┐
│ Orders │ ────────────────────────────────────────────▶ │ Registry │
│ │ ◀──────── 10.2.3.14:8080 ─────────────────── │ │
└──────────┘ └──────────┘3. Inter-Service Communication
Two broad flavors:
- Synchronous (request/response): REST, gRPC. Simple mental model, but creates temporal coupling — if the callee is down, the caller fails.
- Asynchronous (events): Kafka, RabbitMQ, NATS. Services publish events, others react. Loosely coupled, resilient, but harder to reason about.
A mature system uses both. Queries go over gRPC. State changes are published as events.
4. Decentralized Data
Each service owns its database. This is the single biggest commitment you make when going to microservices — and the one most often violated. Sharing a database between services silently recreates a monolith with worse latency.
If service B needs data from service A, it either:
- Asks A over the network (synchronous), or
- Subscribes to A’s events and keeps its own local copy (eventual consistency).
5. Observability
In a monolith, you open one log file. In microservices, a single user request might touch ten services. You need:
- Distributed tracing (Jaeger, Zipkin, OpenTelemetry) — follow a request across services via a trace ID.
- Centralized logs (ELK, Loki) — all services log to one place with a correlation ID.
- Metrics (Prometheus + Grafana) — RED/USE metrics per service.
Without these, debugging production is guesswork.
6. Resilience Patterns
The network will fail. Services will be slow. Plan for it:
- Timeouts. Never make a network call without one.
- Retries with backoff. But only for idempotent operations.
- Circuit breaker. Stop hammering a failing dependency; fail fast instead.
- Bulkheads. Isolate thread pools / connection pools so one slow dependency can’t starve the rest.
- Fallbacks. Serve stale data or a degraded response instead of an error.
Healthy: [Client] ──▶ [Circuit: CLOSED] ──▶ [Service] ✓
Failing: [Client] ──▶ [Circuit: OPEN] ──▶ fail fast ✗
Probing: [Client] ──▶ [Circuit: HALF] ──▶ test one reqCommunication patterns in practice
Orchestration vs. Choreography
When multiple services collaborate on a workflow (e.g. “place order”), you pick one of two styles:
Orchestration — a central coordinator tells each service what to do next.
Orchestrator ──▶ Orders.reserve()
Orchestrator ──▶ Payments.charge()
Orchestrator ──▶ Shipping.schedule()
Orchestrator ──▶ Notifications.send()Easier to see the flow in one place. Downside: the orchestrator becomes a god-service.
Choreography — services react to each other’s events.
Orders publishes OrderCreated ─▶ Payments listens, charges
Payments publishes PaymentSettled ─▶ Shipping listens, schedules
Shipping publishes ShipmentCreated ─▶ Notifications listens, emailsLooser coupling, no single point of failure. Downside: no single place shows the whole workflow.
The Saga pattern
Distributed transactions don’t exist in a useful form across microservices. Instead, you model long-running business transactions as a saga — a sequence of local transactions with compensating actions if a later step fails.
Step 1: Orders.create() ───▶ on failure: (nothing)
Step 2: Payments.charge() ───▶ on failure: Orders.cancel()
Step 3: Shipping.schedule() ───▶ on failure: Payments.refund(), Orders.cancel()The costs you sign up for
Every microservices talk lists the benefits. The costs are less advertised but just as real:
- Operational complexity. You now run tens of services, each with its own deploy, monitoring, alerting, on-call rotation. You need Kubernetes (or equivalent) and a platform team.
- Network is unreliable. Every in-process call becomes a network call that can time out, drop, or return stale data.
- Data consistency is eventual. Forget ACID across services. Model your domain around events and compensations.
- Testing is harder. Unit tests are the same. Integration tests now require spinning up multiple services. End-to-end tests are slow and flaky.
- Debugging is harder. A bug might span five services. Without distributed tracing, good luck.
- Versioning and contracts. Services evolve independently. You need API versioning, consumer-driven contract testing, backward compatibility rules.
- Cost. More databases, more message brokers, more orchestration infrastructure, more engineering time.
When microservices make sense
Use microservices when:
- You have multiple teams that need to ship independently.
- Parts of the system have genuinely different scaling profiles (CPU-bound vs. I/O-bound, 1 RPS vs. 50k RPS).
- You need fault isolation between subsystems (e.g. billing must survive when search crashes).
- Your domain has clear, stable bounded contexts that map to services.
Avoid them when:
- You’re a small team (< ~15 engineers). The overhead will eat your velocity.
- Your domain is unclear. Extracting services from a half-understood domain produces worse boundaries than a well-modularized monolith.
- You don’t already have good CI/CD, monitoring, and infrastructure automation. Microservices multiply these requirements.
Martin Fowler’s “Monolith First” advice still holds: start with a well-structured monolith, find the natural seams, then extract services where the pain is real.
A minimal checklist before going microservices
- Bounded contexts are identified and stable
- CI/CD pipeline can build and deploy services independently
- Centralized logging and distributed tracing are in place
- You have an API gateway (or a plan for one)
- You have a message broker for async communication
- Teams are aligned to service boundaries (Conway’s Law)
- You’re prepared to own the operational overhead
Closing thought
Microservices are not a goal — they’re a trade-off. You give up simplicity to buy team autonomy and independent scaling. If you don’t need what you’re buying, you’re paying the price for nothing. The best microservices architecture is the one that matches your organization’s size, your domain’s complexity, and the problems you actually have today.