Toolmingo
Guides16 min read

Microservices Architecture: A Complete Guide (2025)

Everything you need to know about microservices architecture — what it is, how it works, core patterns, microservices vs monolith, communication, data management, and when to use it.

Microservices architecture is how most large-scale software is built today — Netflix, Amazon, Uber, and Spotify all rely on it. But it's not the right choice for every team or every project. This guide explains exactly what microservices are, how they work, the key patterns you need to know, and when to use (or avoid) them.

At a glance

Monolith Microservices
Structure Single deployable unit Independently deployable services
Codebase One shared codebase Separate repo per service (usually)
Scaling Scale everything together Scale individual services
Deployment Deploy whole app at once Deploy services independently
Tech stack Single stack Polyglot (each service chooses)
Data Shared database Database per service
Communication In-process function calls Network calls (HTTP/gRPC/messages)
Failure scope Bug can take down whole app Failures can be isolated
Complexity Lower operational complexity Higher operational complexity
Team size Works well for small teams Better for large, multiple teams
Best for Startups, simple apps Large orgs, high-scale products

What are microservices?

Microservices is an architectural style where an application is structured as a collection of small, independently deployable services. Each service:

  • Does one thing well (single responsibility)
  • Runs in its own process
  • Communicates over a network (HTTP, gRPC, message queues)
  • Owns its own data (database per service)
  • Can be deployed independently without touching other services
                 ┌─────────────────────────────────────────┐
                 │           Client (Web / Mobile)          │
                 └──────────────────┬──────────────────────┘
                                    │
                 ┌──────────────────▼──────────────────────┐
                 │              API Gateway                  │
                 │   (routing, auth, rate limiting, SSL)     │
                 └────┬──────────┬──────────┬──────────────┘
                      │          │          │
          ┌───────────▼──┐  ┌───▼──────┐  ┌▼──────────────┐
          │  User Service │  │  Order   │  │  Product      │
          │  :8001        │  │  Service │  │  Service      │
          │               │  │  :8002   │  │  :8003        │
          └───────┬───────┘  └───┬──────┘  └──────┬────────┘
                  │              │                  │
          ┌───────▼──┐   ┌───────▼──┐      ┌───────▼──┐
          │  Users DB │   │ Orders DB│      │ Products │
          │ (Postgres)│   │ (MySQL)  │      │ DB (Mongo│
          └───────────┘   └──────────┘      └──────────┘

The "micro" in microservices

The size of a service is less important than its boundaries. A service should map to a bounded context — a coherent business capability. Common guidelines:

  • Can be rewritten in 2 weeks by a small team
  • Follows the "two-pizza team" rule (team small enough to be fed by two pizzas)
  • Has a single, clear responsibility

Microservices vs Monolith vs SOA

Dimension Monolith SOA Microservices
Granularity One large app Large enterprise services Small, focused services
Communication In-process ESB (Enterprise Service Bus) Lightweight (HTTP/gRPC/MQ)
Data Shared DB Often shared DB Database per service
Deployment Deploy whole app Often deploy whole suite Deploy services independently
Team ownership Shared codebase Multiple teams, loose "You build it, you run it"
Coupling Tight Medium (ESB creates coupling) Loose
Technology Single stack Mostly uniform Polyglot
When to use Small teams, early stage Legacy enterprise Large scale, multiple teams

Monolith first (Martin Fowler's advice)

Most experts recommend starting with a monolith:

  1. Start monolith — faster to develop, easier to deploy, simpler operations
  2. Identify pain points — where does the monolith struggle? (scaling, deployment velocity, team conflicts)
  3. Extract services — peel off services where it provides clear value

"Don't start with microservices. Start with a monolith, identify the seams, and extract where it makes sense." — Martin Fowler


Core concepts

Bounded Context (Domain-Driven Design)

Each service owns a bounded context — a specific subdomain of the business:

E-commerce Platform
├── User Context        → User Service (profiles, preferences, auth)
├── Catalog Context     → Product Service (products, categories, search)
├── Order Context       → Order Service (cart, checkout, order lifecycle)
├── Payment Context     → Payment Service (billing, invoicing)
├── Shipping Context    → Shipping Service (logistics, tracking)
└── Notification Context→ Notification Service (email, SMS, push)

Service ownership

The "You build it, you run it" principle from Amazon means:

  • The team that builds a service operates it in production
  • This drives quality and accountability
  • Each service has a clear owner

API contracts

Services communicate via stable API contracts. Breaking changes require versioning:

/api/v1/users/{id}   ← stable, v1 clients still work
/api/v2/users/{id}   ← new version, new response shape

Communication patterns

Synchronous: REST

The most common pattern. Service A calls Service B and waits for a response.

// Order Service calling Product Service
async function getProductDetails(productId) {
  const response = await fetch(
    `http://product-service/api/products/${productId}`
  );
  if (!response.ok) throw new Error('Product not found');
  return response.json();
}

Pros: Simple, familiar, immediate feedback Cons: Temporal coupling — if Product Service is down, Order Service fails too

Synchronous: gRPC

Google's Remote Procedure Call framework. Uses Protocol Buffers (binary) instead of JSON.

// product.proto
service ProductService {
  rpc GetProduct (ProductRequest) returns (ProductResponse);
  rpc ListProducts (ListRequest) returns (stream ProductResponse);
}

message ProductRequest {
  string product_id = 1;
}

message ProductResponse {
  string product_id = 1;
  string name = 2;
  double price = 3;
  bool in_stock = 4;
}

Pros: 3-10x faster than REST, strongly typed, streaming support Cons: Less human-readable, harder to debug, browser support limited

Asynchronous: Message Queues

Services communicate via messages. Producer publishes, consumer subscribes.

Order Service                Message Queue              Notification Service
     │                      (Kafka/RabbitMQ)                   │
     │──── OrderPlaced ────►│                │──── OrderPlaced ─►│
     │                      │                │                   │ (send email)
     │                      │                │                   │
     │                      │                │◄──────────────────│
# Order Service — publish event
import json
from kafka import KafkaProducer

producer = KafkaProducer(bootstrap_servers='kafka:9092')

def place_order(order_data):
    # save to DB...
    order = save_order(order_data)
    
    # publish event
    producer.send('orders', json.dumps({
        'event': 'OrderPlaced',
        'order_id': order.id,
        'user_id': order.user_id,
        'total': order.total,
    }).encode())
    
    return order
# Notification Service — consume event
from kafka import KafkaConsumer

consumer = KafkaConsumer('orders', bootstrap_servers='kafka:9092')

for message in consumer:
    event = json.loads(message.value)
    if event['event'] == 'OrderPlaced':
        send_confirmation_email(event['user_id'], event['order_id'])

Pros: Decoupled, resilient, handles traffic spikes, enables event sourcing Cons: Eventual consistency, harder to debug, requires message broker infrastructure

Communication pattern comparison

Pattern Protocol Coupling Latency Reliability Complexity
REST HTTP/JSON Medium Low–medium Caller handles retry Low
gRPC HTTP/2 + Protobuf Medium Low Caller handles retry Medium
Message Queue Kafka/RabbitMQ Loose Medium (async) At-least-once delivery High
Event Streaming Kafka Very loose Medium Replay possible High
GraphQL Federation HTTP/JSON Medium Low–medium Caller handles retry Medium

API Gateway

Every microservices system needs an API Gateway — the single entry point for all clients.

Clients              API Gateway                  Services
                   ┌─────────────┐
Web App ──────────►│ • Routing   │──────────────► User Service
Mobile App ────────│ • Auth/JWT  │──────────────► Order Service
3rd Party API ─────│ • Rate limit│──────────────► Product Service
                   │ • SSL term  │──────────────► Payment Service
                   │ • Logging   │──────────────► Search Service
                   │ • Caching   │
                   └─────────────┘

Popular API Gateways:

Gateway Type Best for
Kong Open source / Enterprise High performance, plugin ecosystem
AWS API Gateway Cloud managed AWS workloads
Nginx Open source Simple routing, high performance
Traefik Open source Kubernetes-native, auto-discovery
Istio Service mesh Advanced traffic management, mTLS
Envoy Open source proxy Service mesh data plane
Azure API Management Cloud managed Azure workloads

Design patterns

1. Strangler Fig Pattern

Gradually replace a monolith by extracting services piece by piece:

Phase 1: Monolith handles everything
         Client → Monolith (all features)

Phase 2: Extract User Service (strangler proxy in front)
         Client → Proxy → User Service (user routes)
                       → Monolith (everything else)

Phase 3: Extract Order Service
         Client → Proxy → User Service
                       → Order Service
                       → Monolith (remaining features)

Phase N: Monolith retired
         Client → Proxy → User Service
                       → Order Service
                       → Product Service
                       → ... (all extracted)

2. Saga Pattern (Distributed Transactions)

Microservices can't use traditional ACID transactions across services. Sagas solve this with a sequence of local transactions, each publishing events.

Choreography Saga (event-driven):

Order Service        Payment Service      Inventory Service
      │                    │                    │
      │──OrderCreated──────►│                   │
      │                    │──PaymentProcessed──►│
      │                    │                    │──InventoryReserved
      │◄───────────────────────────────────────── (saga complete)

On failure:
      │                    │──PaymentFailed──────►│
      │◄──OrderCompensated──│                    │

Orchestration Saga (central coordinator):

Saga Orchestrator
      │
      ├──1. CreateOrder──────────► Order Service
      │◄──OrderCreated────────────│
      │
      ├──2. ProcessPayment────────► Payment Service
      │◄──PaymentProcessed────────│
      │
      ├──3. ReserveInventory──────► Inventory Service
      │◄──InventoryReserved───────│
      │
      └── Saga Complete

3. Circuit Breaker Pattern

Prevent cascade failures when a service is down:

import time
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"       # normal operation
    OPEN = "open"           # failing, reject requests
    HALF_OPEN = "half_open" # testing recovery

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=30):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failure_count = 0
        self.last_failure_time = None
        self.state = CircuitState.CLOSED

    def call(self, func, *args, **kwargs):
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
            else:
                raise Exception("Circuit is OPEN — service unavailable")

        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise e

    def _on_success(self):
        self.failure_count = 0
        self.state = CircuitState.CLOSED

    def _on_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()
        if self.failure_count >= self.failure_threshold:
            self.state = CircuitState.OPEN

4. Service Discovery

Services register themselves; clients look them up dynamically (no hardcoded IPs).

                 ┌─────────────────┐
Service A ──────►│ Service Registry │◄────── Service B
 (register)      │  (Consul/Eureka) │         (discover A)
                 └─────────────────┘

Types:

Type How it works Examples
Client-side Service queries registry, load-balances itself Netflix Eureka + Ribbon
Server-side Load balancer queries registry AWS ALB + ECS
DNS-based Service registered as DNS entry Kubernetes DNS

5. CQRS (Command Query Responsibility Segregation)

Separate the read model from the write model:

Write side (Commands)         Read side (Queries)
      │                              │
Create Order ─────► Orders DB ──────► Materialised View
Update Order                         (denormalised, fast reads)
Cancel Order         Event Stream ──► Read DB (optimised for queries)

When to use: High-read-to-write ratio, complex domain, reporting requirements

6. Event Sourcing

Store state as a sequence of events, not current state:

Traditional DB:       Event Store:
┌─────────────┐      ┌─────────────────────────────────────────┐
│ orders      │      │ events                                  │
│─────────────│      │─────────────────────────────────────────│
│ id: 123     │  vs  │ OrderCreated   {id:123, user:1, ...}    │
│ status: PAID│      │ PaymentAdded   {id:123, amount:99.99}   │
│ total: 99.99│      │ OrderShipped   {id:123, tracking:XYZ}   │
└─────────────┘      └─────────────────────────────────────────┘
                      Current state = replay of all events

Benefits: Full audit trail, time travel queries, easy event publishing


Data management

Database per service

Each service owns its data exclusively. Other services must call the API — never the DB directly.

✅ CORRECT:
Order Service ──HTTP──► User Service ──► Users DB

❌ WRONG:
Order Service ──────────────────────────► Users DB
              (bypassing User Service)

Handling distributed data challenges

Challenge Solution
No cross-service joins API Composition (aggregate in gateway/client)
No distributed transactions Saga pattern
Data consistency across services Eventual consistency + events
Reporting across services Read replica / data warehouse / CQRS
Referential integrity Soft references by ID, accept eventual consistency

API Composition pattern

// API Gateway aggregates data from multiple services
async function getOrderDetails(orderId) {
  const [order, user, products] = await Promise.all([
    orderService.getOrder(orderId),
    userService.getUser(order.userId),
    productService.getProducts(order.productIds),
  ]);

  return {
    ...order,
    customerName: user.name,
    customerEmail: user.email,
    products: products,
  };
}

Deployment

Containerisation (Docker)

Every microservice runs in a Docker container:

# Dockerfile for Order Service
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production

FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 8002
USER node
CMD ["node", "src/index.js"]

Orchestration (Kubernetes)

Kubernetes manages deployment, scaling, and self-healing:

# order-service deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
      - name: order-service
        image: myregistry/order-service:v1.2.3
        ports:
        - containerPort: 8002
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8002
          initialDelaySeconds: 30
        readinessProbe:
          httpGet:
            path: /ready
            port: 8002
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
  - port: 80
    targetPort: 8002

CI/CD per service

Each service has its own pipeline:

# .github/workflows/order-service.yml
name: Order Service CI/CD
on:
  push:
    paths:
      - 'services/order/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: cd services/order && npm test
      - name: Build and push Docker image
        run: |
          docker build -t myregistry/order-service:${{ github.sha }} .
          docker push myregistry/order-service:${{ github.sha }}
      - name: Deploy to Kubernetes
        run: kubectl set image deployment/order-service order-service=myregistry/order-service:${{ github.sha }}

Observability

With dozens of services, you need observability. The three pillars:

Pillar What it gives you Tools
Logs What happened ELK Stack, Loki + Grafana, Datadog
Metrics How healthy (CPU, memory, req/s, error rate) Prometheus + Grafana, Datadog
Traces Where time was spent (across services) Jaeger, Zipkin, Tempo, Datadog APM

Distributed tracing

A single request may touch 10 services. Tracing follows it end-to-end:

Request ID: abc-123
│
├── API Gateway          10ms
├── User Service         15ms
│   └── Users DB          8ms
├── Order Service        45ms
│   ├── Orders DB        20ms
│   └── Product Service  18ms
│       └── Products DB   10ms
└── Response             70ms total

Structured logging

Every service should output structured logs with a correlation ID:

{
  "timestamp": "2025-01-15T10:30:45Z",
  "level": "info",
  "service": "order-service",
  "trace_id": "abc-123",
  "span_id": "def-456",
  "message": "Order created",
  "order_id": "ord-789",
  "user_id": "usr-456",
  "total": 99.99
}

Challenges

Challenge Description Solution
Network latency In-process calls become network calls Circuit breakers, caching, async messaging
Distributed transactions ACID across services is hard Saga pattern, eventual consistency
Service discovery Services need to find each other Consul, Kubernetes DNS, service mesh
Testing Integration testing across services Contract testing (Pact), consumer-driven contracts
Data consistency No shared DB, eventual consistency Design for it, compensating transactions
Operational complexity Many services to deploy and monitor Kubernetes, service mesh, observability stack
Versioning API changes break consumers API versioning, backward compatibility
Security Service-to-service auth mTLS, service mesh (Istio), JWT
Debugging Tracing a bug across services Distributed tracing, correlation IDs

When to use microservices

Use microservices when:

Scenario Why microservices helps
Multiple teams working on same codebase Independent deployment reduces coordination
Different scaling requirements per feature Scale only the bottleneck service
Need to use different tech stacks Polyglot is possible with microservices
Monolith deploy takes hours Independent deploys speed up delivery
Different availability requirements Critical services can have more replicas
Organisational size > 50 engineers Conway's Law: structure mirrors communication
High-scale traffic (millions of users) Horizontal scaling per service

Don't use microservices when:

Scenario Why monolith is better
Early-stage startup Too much operational overhead, premature optimisation
Small team (< 10 engineers) Microservices complexity exceeds benefits
Simple CRUD app Distributed system is overkill
Monolith works fine "If it ain't broke, don't fix it"
No DevOps maturity You need Kubernetes, CI/CD, monitoring
No domain expertise Hard to find correct service boundaries
Tight deadline Microservices slow down initial development

Microservices vs related architectures

Architecture Granularity Communication Data Best for
Monolith One big app In-process Shared DB Small teams, simple apps
Modular Monolith Modules in one app In-process Shared DB (separate schemas) Medium teams, clear boundaries
SOA Large services ESB / SOAP Often shared Legacy enterprise integration
Microservices Small, focused HTTP/gRPC/MQ DB per service Large teams, high scale
Serverless Functions HTTP / events Stateless Event-driven, variable load
Cell-based Isolated cells APIs Per cell Massive scale, blast radius

Common mistakes

Mistake Problem Fix
Starting with microservices Premature complexity before you know the domain Start monolith, extract services later
Too many services too soon Distributed monolith — still coupled, more overhead Find real domain boundaries first
Sharing databases between services Breaks independence, creates hidden coupling Database per service — always
Synchronous everything Tight coupling, cascading failures Use async messaging for non-critical paths
No API versioning Breaking changes break consumers Version your APIs from day one
Skipping observability Debugging distributed systems without traces is impossible Logs + metrics + traces from day one
Chatty services Too many synchronous calls slow everything down Batch calls, use async, cache aggressively
Ignoring Conway's Law Team structure ≠ service structure → coordination overhead Align teams with service ownership

Quick reference: microservices stack

Layer                   Tools
─────────────────────────────────────────────────────────
Client                  Web, Mobile, 3rd Party
API Gateway             Kong, AWS API GW, Nginx, Traefik
Service Mesh            Istio, Linkerd, Consul Connect
Container Runtime       Docker
Orchestration           Kubernetes (EKS, GKE, AKS)
CI/CD                   GitHub Actions, GitLab CI, ArgoCD
Service Registry        Kubernetes DNS, Consul, Eureka
Message Broker          Kafka, RabbitMQ, AWS SQS/SNS
Logging                 ELK, Loki + Grafana, Datadog
Metrics                 Prometheus + Grafana, Datadog
Tracing                 Jaeger, Tempo, Datadog APM
Config/Secrets          Vault, AWS Secrets Manager, K8s Secrets

FAQ

Q: How small should a microservice be? There's no fixed answer — it depends on the domain. A useful heuristic: a service should do one business capability, be maintainable by a small team (2–5 people), and be deployable independently. If you need to coordinate deploys across multiple "microservices" for a single feature, they're too granular or you've drawn the wrong boundaries.

Q: Can microservices share a database? No — sharing a database defeats the purpose of microservices. It creates tight coupling at the data layer, which means you can't change the schema of one service without breaking others. Database per service is a core rule. For reporting/analytics, use a dedicated read replica or data warehouse that pulls from all services.

Q: How do you handle authentication across services? The API Gateway handles authentication (validates JWT from client). The token is then forwarded to downstream services in headers. Services can trust the token (the Gateway already validated it) or re-validate it themselves. For service-to-service auth, use mutual TLS (mTLS) via a service mesh like Istio.

Q: What's the difference between a microservice and a serverless function? A microservice is a long-running service with its own HTTP server. A serverless function (Lambda, Cloud Functions) is stateless, event-triggered, and runs only when called. Microservices are better for complex business logic, stateful operations, and low-latency requirements. Serverless is better for event-driven, variable-load, and simple transformations. Many systems use both.

Q: How do you test microservices? Use a testing pyramid adapted for distributed systems:

  1. Unit tests — test business logic in isolation (fast, many)
  2. Integration tests — test the service with its real database
  3. Contract tests — verify API contracts between services (Pact)
  4. End-to-end tests — test the whole flow (slow, few)

Contract testing is especially important — it lets teams evolve services independently without breaking consumers.

Q: Is Kubernetes required for microservices? No, but it makes life much easier. You can run microservices on VMs, bare metal, Docker Swarm, or serverless platforms. For small-scale systems, Docker Compose works for local development and simple deployments. For production at scale, Kubernetes is the industry standard because it handles scheduling, self-healing, scaling, service discovery, and rolling deployments.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools