Microservices architecture is a design approach where an application is composed of small, independently deployable services, each owning a specific business domain. When implemented well, microservices enable teams to ship independently, scale individual components, and choose the right technology for each problem. When implemented poorly, they create a distributed monolith that is slower and harder to debug than the monolith it replaced.
When Microservices Make Sense (and When They Do Not)
Microservices add operational complexity. Before adopting them, verify that the benefits outweigh the costs for your team and product stage.
| Factor | Microservices Fit | Monolith Fit |
| Team size | 15+ engineers, multiple teams | 1-10 engineers, single team |
| Deployment frequency | Multiple deploys per day per service | Weekly or bi-weekly releases |
| Scaling requirements | Different components scale independently | Uniform scaling sufficient |
| Technology diversity | Different languages/runtimes needed | Single technology stack |
| Domain complexity | Clear, separable business domains | Tightly coupled domain logic |
| Organizational structure | Conway's Law alignment needed | Single team owns everything |
The most common mistake is adopting microservices too early, before the team has clear domain boundaries or operational maturity. Start with a well-structured monolith, then extract services when you have evidence that independent deployment or independent scaling is needed.
Pattern 1: Domain-Driven Service Boundaries
Service boundaries should align with business domains, not technical layers. A "user service" that owns user registration, authentication, profile management, and user preferences is better than separate services for each of those functions.
How to identify service boundaries:
Good service boundary indicators:
- The service owns its data completely (no shared database tables)
- Changes to this service rarely require changes to other services
- A single team can own and operate the service end-to-end
- The service has a clear, stable API contract
Bad service boundary indicators:
- Two services must always be deployed together
- A change requires updating 3+ services simultaneously
- Services make synchronous calls to each other in tight loops
- Data is duplicated across services with manual sync processes
For platform options that support independent service deployment, see Services Overview.
Pattern 2: Contract-First API Design
Define API contracts before writing implementation code. This enables parallel development across teams, automated compatibility testing, and clear documentation.
<h1>OpenAPI contract-first example</h1>
openapi: 3.0.3
info:
title: Order Service API
version: 1.0.0
paths:
/orders:
post:
summary: Create a new order
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderRequest"
responses:
"201":
description: Order created
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"400":
description: Invalid request
Contract-first workflow:
API Versioning Strategies
| Strategy | Pros | Cons | Best For |
| URL versioning (/v1/orders) | Simple, explicit | URL changes on every version | External APIs |
| Header versioning (Accept: v2) | Clean URLs | Less discoverable | Internal APIs |
| Additive changes only | No versioning overhead | Cannot remove fields | Most services |
For most internal microservices, additive-only changes with deprecation notices eliminate the need for explicit versioning. Only introduce version numbers when breaking changes are unavoidable.
Pattern 3: Asynchronous Messaging for Decoupling
Synchronous HTTP calls between services create tight coupling and cascading failures. When Service A calls Service B synchronously, an outage in B causes an outage in A. Asynchronous messaging breaks this dependency.
Use async messaging for:
- Event notification: "An order was placed" (other services react independently)
- Command distribution: "Process this payment" (producer does not wait for result)
- Data synchronization: "Customer address changed" (consuming services update their copies)
<h1>Event publishing example</h1>
async def create_order(order_data):
# Save to local database
order = await db.orders.create(order_data)
# Publish event (async, non-blocking)
await event_bus.publish("order.created", {
"order_id": order.id,
"customer_id": order.customer_id,
"total": order.total,
"items": order.items
})
return order
Message broker comparison:
| Broker | Ordering | Persistence | Throughput | Best For |
| Apache Kafka | Partition-ordered | Durable log | Very High | Event streaming, audit logs |
| RabbitMQ | Queue-ordered | Optional | High | Task distribution, RPC |
| NATS JetStream | Stream-ordered | Optional | Very High | Cloud-native, lightweight |
| Redis Streams | Stream-ordered | Configurable | High | Simple event bus, low latency |
Pattern 4: Saga Pattern for Distributed Transactions
In a microservices architecture, you cannot use traditional database transactions across services. The Saga pattern coordinates multi-service workflows through a sequence of local transactions with compensating actions for rollback.
Example: Order placement saga
If step 3 fails:
Saga Implementation Approaches
| Approach | Coordination | Complexity | Best For |
| Choreography | Events between services | Lower | Simple, few-step sagas |
| Orchestration | Central saga coordinator | Higher | Complex, multi-step sagas |
Choreography works well for 2-3 step sagas. Beyond that, orchestration provides better visibility into saga state and easier debugging.
Pattern 5: Platform Standards for Operational Consistency
Without platform standards, each microservice team reinvents logging, metrics, health checks, deployment, and configuration management. This creates operational chaos at scale.
Define and enforce standards for:
- Health check endpoints: Standard /health and /ready paths with consistent response format
- Logging format: JSON structured logging with required fields (timestamp, level, service, trace_id)
- Metrics exposition: Prometheus-compatible /metrics endpoint with RED metrics
- Configuration management: Environment variables for secrets, config files for everything else
- Deployment manifests: Standard Kubernetes resource templates with required labels and annotations
- Error response format: Consistent error codes, messages, and correlation IDs across all services
// Standard error response format
{
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order with ID 12345 does not exist",
"trace_id": "abc123def456",
"timestamp": "2025-03-03T10:30:00Z"
}
}
For managed deployment standards and service lifecycle, explore Cloud Computing.
Anti-Pattern 1: Shared Database
Multiple services reading from and writing to the same database tables is the most common microservices anti-pattern. It creates hidden coupling: a schema change in one service breaks another.
Symptoms:
- Services cannot be deployed independently because they share schema migrations
- Performance issues in one service affect others sharing the database
- Teams cannot change their data model without coordinating with other teams
Fix: Each service owns its database. If services need each other's data, they request it through APIs or subscribe to domain events.
Anti-Pattern 2: Distributed Monolith
A distributed monolith has the complexity of microservices with none of the benefits. Services are separately deployed but tightly coupled through synchronous call chains, shared libraries, or coordinated releases.
Symptoms:
- Deploying one service requires deploying 2-3 others simultaneously
- A change in one service requires updating shared libraries used by other services
- Services cannot function when any of their dependencies are down
Fix: Introduce async messaging, eliminate shared libraries (duplicate code is acceptable), and redesign boundaries around true domain independence.
Anti-Pattern 3: Nano-Services
Over-decomposing into too many tiny services creates excessive network overhead, complex debugging, and deployment coordination problems. A service that is only 50-100 lines of code is probably too small.
Symptoms:
- A single user request hits 10+ services
- Latency is dominated by network hops, not business logic
- Teams spend more time on inter-service communication than on features
Fix: Merge related nano-services back into cohesive domain services. A service should own a meaningful business capability, not a single function.
Testing Microservices
The testing pyramid shifts significantly in a microservices architecture. Integration tests and contract tests become more important than unit tests.
| Test Type | Scope | Speed | Confidence | Priority |
| Unit tests | Single function/class | Fast | Low for integration | Medium |
| Contract tests | API compatibility | Fast | High for contracts | High |
| Integration tests | Service + dependencies | Medium | High for behavior | High |
| End-to-end tests | Full user journey | Slow | Highest | Low (few, critical paths) |
Contract testing with tools like Pact ensures that service API changes do not break consuming services without requiring full end-to-end test suites.
Review Documentation for testing framework guides and implementation patterns.
Migration Strategy: Monolith to Microservices
The strangler fig pattern is the safest approach for migrating from a monolith to microservices:
Critical rules for migration:
- Never do a big-bang rewrite. Extract one service at a time.
- Keep the monolith working throughout the migration. It is your fallback.
- Extract the highest-value service first to prove the pattern.
- Accept that migration will take months or years, not weeks.
Compare infrastructure options for your migration at Pricing. For architecture review and planning, Contact us.
Frequently Asked Questions
Q: How many microservices should we have?
There is no magic number. A good heuristic is one service per team (5-8 engineers). If a single team owns more than 3-4 services, they are probably too granular. If a service requires more than one team to develop and deploy, it is probably too large.
Q: Should microservices share code through libraries?
Minimize shared libraries. Shared client libraries for API communication are acceptable. Shared business logic libraries create coupling and coordinated release requirements. When in doubt, duplicate code rather than share it.
Q: How do we handle data consistency across microservices?
Accept eventual consistency as the default. Use the Saga pattern for workflows that span multiple services. Design services to handle out-of-order events and duplicate messages (idempotency). Only use synchronous calls when immediate consistency is truly required.
Q: What is the right communication protocol between services?
Use HTTP/REST or gRPC for synchronous request-response. Use message brokers (Kafka, RabbitMQ, NATS) for asynchronous events. gRPC offers better performance and type safety for internal service communication. REST is simpler and better for external APIs.
Q: How do we debug issues that span multiple microservices?
Distributed tracing is essential. Propagate trace IDs through all service calls and include them in log entries. Tools like Jaeger or Tempo visualize the full request path. Without tracing, debugging cross-service issues is extremely difficult.