Microservices Architecture#

Microservices decompose a system into small, independently deployable services, each owning a specific business capability. This architectural style enables teams to move fast and scale independently — but introduces significant distributed systems complexity.


Monolith vs Microservices#

AspectMonolithMicroservices
DeploymentSingle deployable unitEach service deployed independently
ScalingScale entire applicationScale individual services
TechnologySingle tech stackPolyglot (per-service choice)
DataShared databaseDatabase per service
Team structureOne team or many teams on one codebaseSmall teams per service (2-pizza rule)
ComplexityIn-process calls, simpleNetwork calls, distributed complexity
ConsistencyStrong (ACID transactions)Eventual consistency (sagas)
TestingEnd-to-end straightforwardIntegration testing complex
LatencyIn-process (nanoseconds)Network (milliseconds)
DebuggingSingle process, stack tracesDistributed tracing required
Initial velocityFaster to startSlower to start (infrastructure overhead)
Long-term velocitySlows as codebase growsMaintains speed per service

The Monolith Is Not the Enemy#

A well-structured monolith (modular monolith) is a valid architecture. Many successful systems start as monoliths and only decompose when specific scaling or team boundaries demand it.

graph TD
    subgraph "Modular Monolith"
        direction TB
        A[Orders Module] --- B[Inventory Module]
        B --- C[Payments Module]
        C --- D[Shipping Module]
    end
    subgraph "Microservices"
        direction TB
        E[Orders Service] -.- F[Inventory Service]
        F -.- G[Payments Service]
        G -.- H[Shipping Service]
    end

Service Boundaries (Domain-Driven Design)#

The hardest part of microservices is deciding where to draw service boundaries. Domain-Driven Design (DDD) provides a framework.

Bounded Contexts#

A bounded context is a logical boundary within which a particular domain model applies. Each microservice should align with one bounded context.

graph TD
    subgraph "Order Context"
        O1[Order]
        O2[OrderItem]
        O3[OrderStatus]
    end
    subgraph "Inventory Context"
        I1[Product]
        I2[StockLevel]
        I3[Warehouse]
    end
    subgraph "Shipping Context"
        S1[Shipment]
        S2[Carrier]
        S3[TrackingEvent]
    end

Heuristics for Finding Boundaries#

SignalSuggests Separate Service
Different rate of changeOrders change weekly, catalog changes monthly
Different scaling needsSearch handles 100x the traffic of checkout
Different team ownershipTeam A owns pricing, Team B owns fulfilment
Different data models“Customer” means different things in billing vs marketing
Independent deployabilityCan this change without coordinating with others?

Anti-Pattern: Entity Services#

Don’t decompose by entities (UserService, OrderService, ProductService). This creates an anaemic architecture where every operation requires multiple services. Decompose by capability: OrderManagement, Fulfilment, Pricing.


Communication Patterns#

Synchronous: HTTP/REST and gRPC#

PropertyREST (HTTP/JSON)gRPC (HTTP/2 + Protobuf)
FormatJSON (human-readable)Protobuf (binary, compact)
PerformanceHigher latency, larger payloadsLower latency, smaller payloads
StreamingLimited (WebSockets separate)Native bidirectional streaming
SchemaOpenAPI (optional)Protobuf IDL (mandatory)
Browser supportNativeRequires gRPC-web proxy
Use casePublic APIs, simple CRUDInternal service-to-service, high throughput

Asynchronous: Event-Driven Messaging#

sequenceDiagram
    participant Order
    participant Queue
    participant Inventory
    participant Shipping
    
    Order->>Queue: OrderPlaced event
    Queue->>Inventory: Consume event
    Inventory->>Inventory: Reserve stock
    Queue->>Shipping: Consume event
    Shipping->>Shipping: Prepare shipment

When to Use Which#

ScenarioPattern
User needs immediate responseSync (REST/gRPC)
Fire-and-forget notificationsAsync (event)
Long-running operationsAsync + polling/callback
Multiple services need same dataPub/sub events
Query across servicesAPI composition or CQRS

API Gateway Pattern#

An API gateway sits between clients and microservices, providing a single entry point.

graph LR
    Client[Mobile/Web Client] --> GW[API Gateway]
    GW --> Auth[Auth Service]
    GW --> Orders[Order Service]
    GW --> Products[Product Service]
    GW --> Users[User Service]

Responsibilities#

ResponsibilityDescription
RoutingRoute requests to appropriate service
AuthenticationVerify tokens, reject unauthorized requests
Rate limitingProtect services from traffic spikes
Request aggregationCompose responses from multiple services
Protocol translationREST externally, gRPC internally
SSL terminationHandle TLS at the edge
CachingCache GET responses to reduce backend load

BFF (Backend for Frontend)#

A specialisation where each client type (web, mobile, IoT) has its own gateway optimised for that client’s needs. The mobile BFF returns smaller payloads; the web BFF supports richer responses.


Service Discovery#

In dynamic environments (containers, auto-scaling), service locations change frequently. Service discovery solves “how does Service A find Service B?”

ApproachHow It WorksExamples
Client-side discoveryClient queries registry, picks instanceNetflix Eureka, Consul
Server-side discoveryLoad balancer queries registryAWS ALB, Kubernetes Services
DNS-basedServices registered as DNS recordsAWS Cloud Map, Consul DNS
Platform-nativeOrchestrator provides discoveryKubernetes DNS, ECS Service Connect

In Kubernetes, service discovery is built-in:

http://order-service.namespace.svc.cluster.local:8080

Data Ownership: Database per Service#

Each microservice owns its data exclusively. No other service may access another’s database directly.

Rules#

  1. Private database — each service has its own DB (or schema)
  2. No shared tables — if two services need the same data, they sync via events
  3. API access only — query another service’s data through its API, never its DB
  4. Eventual consistency — accept that data across services may be temporarily inconsistent

Data Duplication is Acceptable#

Services may maintain local copies of data they frequently need. The shipping service keeps a local copy of customer addresses — this avoids synchronous calls to the customer service on every shipment.


Saga Pattern for Distributed Transactions#

With database-per-service, you can’t use ACID transactions across services. The saga pattern manages distributed transactions as a sequence of local transactions with compensating actions.

Choreography (Event-Based)#

Each service listens for events and reacts:

sequenceDiagram
    participant Order
    participant Payment
    participant Inventory
    
    Order->>Order: Create order (PENDING)
    Order-->>Payment: OrderCreated
    Payment->>Payment: Charge card
    Payment-->>Inventory: PaymentCompleted
    Inventory->>Inventory: Reserve stock
    Inventory-->>Order: StockReserved
    Order->>Order: Confirm order (CONFIRMED)
    
    Note over Payment,Inventory: If payment fails:
    Payment-->>Order: PaymentFailed
    Order->>Order: Cancel order (CANCELLED)

Orchestration (Central Coordinator)#

A saga orchestrator tells each service what to do and handles compensations on failure.

ApproachProsCons
ChoreographyDecoupled, no single point of failureHard to track, complex flows unclear
OrchestrationClear flow, easy to understandCentral coordinator is a coupling point

Service Mesh#

A service mesh manages service-to-service communication through sidecar proxies, extracting cross-cutting concerns from application code.

What a Service Mesh Provides#

FeatureWithout MeshWith Mesh
mTLSApplication-level TLS configAutomatic, transparent
RetriesEach service implements retry logicConfigured in mesh policy
Circuit breakingLibrary (e.g., Resilience4j)Proxy-level, language-agnostic
ObservabilityInstrument each serviceAutomatic metrics/traces from proxies
Traffic splittingApplication routing logicMesh config (canary, A/B)
Rate limitingPer-service implementationMesh-wide policy

Popular service meshes: Istio, Linkerd, Consul Connect, AWS App Mesh.

Use a service mesh when you have 10+ services, polyglot stacks, or strong security requirements (mTLS everywhere).


When NOT to Use Microservices#

Microservices are not a default — they are a tool for specific problems.

SituationWhy Microservices Hurt
Small team (< 5 engineers)Operational overhead exceeds benefit
Unclear domain boundariesYou’ll draw boundaries wrong and pay the refactoring cost
Early-stage startupNeed to iterate fast; monolith is faster to change
No DevOps maturityNeed CI/CD, monitoring, container orchestration first
Strong consistency requiredDistributed transactions are painful
Low traffic / simple appComplexity unjustified by scale needs

Start Monolith, Extract Later#

  1. Build a well-structured modular monolith
  2. Define clear module boundaries (packages/namespaces)
  3. Extract to microservices only when you feel the pain: team conflicts, deployment coupling, or independent scaling needs

Key Takeaways#

  • Microservices trade code complexity for operational complexity — make sure you can afford the operational cost
  • Service boundaries should follow business capabilities (bounded contexts), not technical layers or entities
  • Database per service is non-negotiable — shared databases destroy independence
  • Prefer async communication between services; use sync only when the caller truly needs an immediate response
  • Sagas replace transactions in distributed systems — design compensating actions for every step
  • API gateways provide a stable client interface while allowing internal service evolution
  • A well-structured monolith is better than poorly-designed microservices — don’t split prematurely
  • Service meshes extract cross-cutting concerns but add infrastructure complexity — adopt only at sufficient scale