|$ curl https://forge-ai.dev/api/markdown?path=docs/system-design/microservices
$cat docs/microservices.md
updated Recently·30 min read·published

Microservices

System DesignAdvanced🎯Free Tools
Introduction

Microservices architecture structures an application as a collection of loosely coupled, independently deployable services. Each service owns a bounded context, has its own data store, and communicates with other services over a network. The goal is to enable team autonomy, independent scaling, and technology diversity.

Microservices are not a free lunch. They trade development simplicity for operational complexity. A system that fits comfortably in a well-modularized monolith is often faster to build, test, and operate than an equivalent microservices system run by a small team.

This guide covers how to identify service boundaries, how services communicate, how they find each other, and the tradeoffs that determine whether microservices are the right choice for your organization.

Monolith vs Microservices

A monolith is a single deployable unit that contains all functionality. Calls between modules are in-process function calls. A microservices architecture splits functionality into separate processes that communicate over the network.

DimensionMonolithMicroservices
DeploymentSingle artifactMany independent artifacts
CommunicationIn-process function callsNetwork calls (HTTP, gRPC, messaging)
Data ownershipShared databaseDatabase per service
Team autonomyCoordination requiredIndependent release cadence
Operational complexityLowHigh — observability, deployments, failures
ScalabilityScale the whole unitScale individual services

best practice

Start with a modular monolith. Clear internal boundaries make a future split possible. Split into services only when organizational scale, deployment cadence, or independent scaling requirements justify the operational cost.
Defining Service Boundaries

The hardest part of microservices is drawing the boundaries. A boundary should align with a business capability, not a technical layer. Services organized by function — orders, payments, inventory — are more stable than services organized by layer — frontend, backend, database.

Bounded Contexts

From Domain-Driven Design, a bounded context is a linguistic boundary within which a domain model is consistent. The word "customer" might mean something different in billing than in support. Each bounded context is a candidate for a microservice.

bounded-contexts.txt
TEXT
1E-commerce bounded contexts:
2
3┌─────────────┐ ┌─────────────┐ ┌─────────────┐
4│ Catalog │ │ Cart │ │ Orders │
5│ (products) │ │ (session) │ │ (checkout) │
6└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
7 │ │ │
8 └───────────────┼───────────────┘
9
10 ┌─────────▼─────────┐
11 │ Payments │
12 │ (charge/refund) │
13 └───────────────────┘
14
15Each context owns its own data and exposes
16a stable API to other contexts.

Signs of a Bad Boundary

Services that constantly call each other in tight loops
Services that share a database and coordinate transactions
Services that must be deployed together
Services that are just CRUD wrappers around a table

warning

If two services cannot be changed or deployed independently, they are not really separate services. Merge them until the boundary is clean.
Inter-Service Communication

Services communicate either synchronously or asynchronously. Synchronous calls are simpler to reason about but create temporal coupling: if the downstream service is slow or unavailable, the caller blocks or fails. Asynchronous communication decouples services through message queues or event streams.

Synchronous

HTTP/REST and gRPC are the dominant synchronous protocols. REST is human-readable and widely supported. gRPC is faster and strongly typed but requires HTTP/2 and protobuf tooling. Reserve synchronous calls for operations where the response is needed immediately and the dependency is reliable.

async-communication.txt
TEXT
1Synchronous request chain:
2
3Client ──▶ API Gateway ──▶ Order Service ──▶ Payment Service
4
5
6 Inventory Service
7
8Risk: any failure in the chain fails the request.
9Mitigation: timeouts, retries, circuit breakers."}
10 filename="sync-communication.txt"
11 />
12
13 <h3 className="text-sm font-heading font-medium text-[#E0E0E0] mt-6 mb-3">Asynchronous</h3>
14 <p className="text-xs font-mono text-[#A0A0A0] leading-relaxed mb-4">
15 Asynchronous communication uses message queues or event buses. A service publishes an event and continues; other services consume the event when ready. This improves resilience and throughput but introduces eventual consistency and the need for idempotent consumers.
16 </p>
17
18 <CodeBlock
19 language="text"
20 code={`Asynchronous event flow:
21
22Order Service publishes OrderPlaced event
23
24
25 ┌─────────────┐
26 │ Message │
27 │ Broker │
28 └──────┬──────┘
29
30 ┌────────┼────────┐
31 ▼ ▼ ▼
32Payment Inventory Notification
33Service Service Service
StyleCouplingLatencyResilienceBest For
Sync RESTTightLowRequires retries/breakersUser-facing queries
Sync gRPCTightVery lowRequires retries/breakersInternal service calls
Async queueLooseHigherHigh — messages persistBackground work, buffering
Event streamLooseNear-real-timeHigh — replayableEvent sourcing, analytics

best practice

Prefer async communication for writes that do not need an immediate response. Use sync communication only for reads and operations where the user is waiting. Never let a synchronous chain grow deeper than two or three hops.
Service Discovery

In a dynamic environment, service instances come and go. Service discovery lets clients locate healthy instances without hard-coding addresses. There are two main models: client-side discovery, where the client queries a registry and picks an instance, and server-side discovery, where a load balancer or proxy routes requests.

service-discovery.txt
TEXT
1Server-side discovery with a load balancer:
2
3Client ──▶ API Gateway / LB ──▶ Registry ──▶ Healthy instances
4 (Consul,
5 Eureka,
6 Kubernetes DNS)
7
8Client-side discovery:
9
10Client ──▶ Registry ──▶ picks instance
11
12 └── caches list, refreshes periodically

info

In Kubernetes, DNS and headless services provide service discovery out of the box. For heterogeneous environments, a dedicated registry like Consul or a service mesh like Istio adds health checking, routing rules, and mTLS.
Database per Service

Each microservice should own its data. No other service should read or write that service's tables directly. Data access happens only through the service's API. This encapsulation is what makes services independently evolvable.

The tradeoff is that operations spanning multiple services cannot use a single database transaction. You must design for eventual consistency using sagas, outbox patterns, or compensation transactions.

data-ownership.txt
TEXT
1Anti-pattern: shared database
2
3Service A ──┐
4 ├──▶ Shared Database
5Service B ──┘
6
7Result: schema changes in one service break another.
8
9Pattern: database per service
10
11Service A ──▶ Database A
12Service B ──▶ Database B
13
14Integration only through APIs or events.

warning

A shared database between microservices is a distributed monolith in disguise. It couples schema changes, prevents independent deployment, and makes ownership unclear.
Microservices Patterns

Several patterns help manage the complexity of distributed systems. Knowing when to apply them separates a working microservices architecture from a fragile one.

API Gateway

An API Gateway is the single entry point for clients. It handles authentication, rate limiting, request routing, protocol translation, and response aggregation. It shields clients from the internal service topology.

Circuit Breaker

A circuit breaker stops calls to a failing service, giving it time to recover and preventing cascading failures. After a cooldown, it allows a trickle of traffic to test health before fully closing.

Saga Pattern

A saga coordinates a long-running business transaction across services using a sequence of local transactions. If one step fails, compensating transactions undo previous steps. Orchestrated sagas use a central coordinator; choreographed sagas use events.

Outbox Pattern

The outbox pattern ensures reliable event publishing. A service writes events to an outbox table in the same database transaction as the business data. A separate process reads the outbox and publishes events to a message broker.

🔥

pro tip

Do not implement microservices patterns for their own sake. Use them to solve specific problems: a gateway to simplify clients, a circuit breaker to stop cascades, a saga to replace distributed transactions.
Observability

In a distributed system, failures are inevitable and debugging is harder. Observability — logs, metrics, and traces — is non-negotiable. Distributed tracing in particular follows a request across service boundaries, revealing latency hotspots and failure points.

SignalAnswersTools
LogsWhat happened in a service?ELK, Loki, CloudWatch
MetricsHow is the system behaving over time?Prometheus, Datadog, Grafana
TracesWhere did a request spend time?Jaeger, Zipkin, Honeycomb

warning

You cannot operate microservices without observability. Build it in from day one, not as an afterthought. Every service should emit structured logs, expose metrics, and propagate trace context.
$Blueprint — Engineering Documentation·Section ID: SD-MS-01·Revision: 1.0