Back to Blog

Microservices Architecture: When and How

A pragmatic guide to microservices architecture, covering when it makes sense, how to design service boundaries, and how to avoid common pitfalls.

The Microservices Question

Every growing engineering team eventually asks: should we move to microservices? The honest answer is more nuanced than most articles suggest. Microservices solve real problems, but they introduce real complexity. The right architecture depends on your team size, your domain, and your operational maturity.

This guide helps you decide whether microservices are appropriate for your situation, and if they are, how to implement them well.

When Microservices Make Sense

Good Reasons to Adopt

Independent deployment: Different teams need to deploy their components on different schedules without coordinating with every other team.

Technology diversity: Some parts of your system benefit from different technology choices. A machine learning pipeline in Python, a real-time API in Go, and a web application in Node.js can each use the best tool for the job.

Scaling independence: Your order processing service needs 20 instances during a flash sale, but your product catalog service needs only 2. Microservices let you scale each component independently.

Team autonomy: Teams of 5 to 8 people own entire services end-to-end: development, testing, deployment, and monitoring. This ownership model reduces coordination overhead.

Fault isolation: When the recommendation engine crashes, it should not take down the checkout flow.

When to Stay Monolithic

Small team: If your entire engineering team is under 10 people, a well-structured monolith is almost always the better choice. The coordination overhead of microservices outweighs the benefits.

Early-stage product: When you are still discovering what your product should do, microservices lock you into service boundaries prematurely. Build a monolith first, then extract services as stable boundaries emerge.

Limited operational capability: Microservices require sophisticated monitoring, logging, deployment pipelines, and incident response. If you do not have these in place, microservices will create chaos.

Designing Service Boundaries

Domain-Driven Design

The most reliable method for identifying service boundaries comes from Domain-Driven Design (DDD). Each service aligns with a bounded context: a part of the business domain with its own internal consistency rules.

For an e-commerce platform:

Bounded Contexts:
├── Catalog Service
│   Owns: products, categories, pricing
│   Data: product descriptions, images, prices

├── Order Service
│   Owns: orders, order lines, order status
│   Data: order details, fulfillment state

├── Inventory Service
│   Owns: stock levels, warehouse locations
│   Data: quantities, reservations, movements

├── Customer Service
│   Owns: customer profiles, addresses
│   Data: contact info, preferences

├── Payment Service
│   Owns: payment processing, refunds
│   Data: payment methods, transaction records

└── Notification Service
    Owns: email, SMS, push notifications
    Data: templates, delivery status

The Boundary Test

A good service boundary passes these checks:

  1. The service can be developed by one team (5 to 8 people)
  2. Changes within the service rarely require changes in other services
  3. The service has its own data store that other services cannot access directly
  4. The service’s API is stable and does not change frequently
  5. The service can be deployed independently without coordination

If a change frequently requires updating multiple services simultaneously, those services are probably too tightly coupled and should either be merged or their boundaries redrawn.

Communication Patterns

Synchronous: REST and gRPC

For request-response interactions where the caller needs an immediate answer:

// REST: Simple, human-readable, widely supported
GET /api/products/12345
Response: { "id": "12345", "name": "Widget", "price": 25000 }

// gRPC: Binary protocol, faster, strongly typed
service ProductService {
  rpc GetProduct(ProductRequest) returns (Product);
  rpc ListProducts(ListRequest) returns (stream Product);
}

Use REST for public-facing APIs and gRPC for internal service-to-service communication where performance matters.

Asynchronous: Event-Driven

For interactions where the caller does not need an immediate response:

// Order Service publishes an event
{
  "event": "order.created",
  "timestamp": "2026-01-15T10:30:00Z",
  "data": {
    "orderId": "ORD-2026-001",
    "customerId": "CUST-456",
    "items": [
      { "productId": "PROD-789", "quantity": 2 }
    ],
    "totalAmount": 500000
  }
}

// Inventory Service subscribes and reserves stock
// Payment Service subscribes and initiates payment
// Notification Service subscribes and sends confirmation email

Event-driven architecture decouples services temporally. The Order Service does not wait for inventory, payment, or notification to complete. Each service processes the event at its own pace.

Choosing the Right Pattern

ScenarioPatternReason
User requests product detailsSynchronous RESTImmediate response needed
Service needs to validate inventorySynchronous gRPCFast internal call
Order placed, need to notify systemsAsynchronous eventMultiple consumers, no blocking
Data synchronization between servicesAsynchronous eventEventually consistent is acceptable
Complex workflow spanning servicesSaga patternDistributed transaction coordination

Data Management

Database Per Service

Each microservice owns its data and exposes it only through its API. No service directly queries another service’s database:

Order Service      → orders_db (PostgreSQL)
Catalog Service    → catalog_db (PostgreSQL)
Search Service     → search_index (Elasticsearch)
Session Service    → sessions (Redis)
Analytics Service  → analytics_db (ClickHouse)

This isolation means services can choose the database technology that best fits their access patterns. It also means no service can break another by modifying shared data.

Handling Data Consistency

In a monolith, a single database transaction ensures consistency. In microservices, you need different strategies:

Saga pattern: A sequence of local transactions coordinated through events. If step 3 fails, compensating transactions undo steps 1 and 2.

Order Saga:
1. Order Service: Create order (status: pending)
2. Payment Service: Process payment
   → Success: Continue
   → Failure: Order Service cancels order (compensating)
3. Inventory Service: Reserve stock
   → Success: Order Service confirms order
   → Failure: Payment Service refunds (compensating),
              Order Service cancels order (compensating)

Event sourcing: Store the sequence of events rather than the current state. Any service can rebuild its view of the data by replaying events.

Observability

Distributed Tracing

When a request flows through five services, you need to trace it end-to-end:

Request: GET /api/orders/ORD-2026-001
Trace ID: abc-123-def-456

├── API Gateway (12ms)
├── Order Service (45ms)
│   ├── Database query (8ms)
│   └── Customer Service call (25ms)
│       └── Database query (5ms)
├── Inventory Service call (18ms)
│   └── Database query (6ms)
└── Total: 75ms

Distributed tracing tools (Jaeger, Zipkin, AWS X-Ray) instrument your services to propagate trace context across service boundaries.

Centralized Logging

Logs from all services must flow to a centralized system. Include the trace ID in every log message so you can correlate logs across services for a single request:

{
  "timestamp": "2026-01-15T10:30:00.123Z",
  "service": "order-service",
  "traceId": "abc-123-def-456",
  "level": "info",
  "message": "Order created",
  "orderId": "ORD-2026-001",
  "customerId": "CUST-456"
}

Health Checks and Alerting

Every service needs:

  • A health check endpoint (/health) that reports service status
  • Metrics endpoints or push-based metric collection (Prometheus)
  • Alerts on error rates, latency, and resource utilization
  • Runbooks for common failure scenarios

Common Pitfalls

The Distributed Monolith

If every change requires coordinated deployments across multiple services, you have a distributed monolith. This is worse than a regular monolith because you have all the complexity of distributed systems with none of the benefits.

Signs of a distributed monolith:

  • Services share a database
  • Services must be deployed in a specific order
  • Changes frequently span multiple services
  • Services call each other synchronously in long chains

Too Many Services Too Soon

Starting with 30 microservices when you have 15 engineers creates operational chaos. The overhead of managing that many services, databases, deployment pipelines, and monitoring dashboards overwhelms the team.

Start with a modular monolith. Extract services one at a time as clear boundaries emerge and operational tooling matures.

Ignoring Network Reality

Networks fail. Latency spikes. Messages get duplicated or lost. Microservices that do not account for network unreliability will exhibit bizarre failures in production.

Every inter-service call needs:

  • Timeouts (never wait forever)
  • Retries with exponential backoff
  • Circuit breakers to prevent cascade failures
  • Idempotency for safe retries

A Pragmatic Migration Path

If you are moving from a monolith to microservices:

  1. Modularize the monolith first: Establish clear internal boundaries, separate data access layers, define internal APIs between modules
  2. Build operational infrastructure: Monitoring, logging, CI/CD, container orchestration
  3. Extract the first service: Choose something with a clear boundary and low coupling to the rest. Notifications or search are common starting points.
  4. Learn and iterate: The first extraction will reveal organizational and technical challenges. Address them before extracting the next service.
  5. Continue extracting as justified: Not every module needs to become a service. Stop when the remaining monolith is manageable and the extracted services are delivering value.

The goal is not zero monolith. The goal is the right architecture for your team, your product, and your stage of growth.

Baca dalam Bahasa Indonesia Versi Indonesia