Microservices Design Patterns
Microservices Design Patterns
Patterns Covered:
Decomposition • Data Management • Communication • Reliability
Security • Observability • Deployment • UI Patterns
1. Microservices Design Patterns — Overview
Design patterns in microservices are proven, reusable solutions to recurring architectural problems.
Unlike OOP patterns (GoF), microservice patterns address the unique challenges of distributed systems:
network partitions, partial failures, data consistency across service boundaries, and operational
complexity.
Decomposition Business Capability, Subdomain, Strangler How to split the system into
Fig, Anti-Corruption Layer services
Data DB per Service, Shared DB, Saga, CQRS, Data consistency & storage
Management Event Sourcing, Outbox isolation
Communication API Gateway, BFF, Service Mesh, How services talk to each other
Messaging, Request-Reply
Security Access Token, API Key, Service-to-Service Auth & authorization across
Auth, Secrets Mgmt services
Observability Log Aggregation, Distributed Tracing, Health Understanding what the system is
Check, Alerting doing
Deployment Blue/Green, Canary, Rolling Update, Sidecar, Releasing and running services
Service Discovery safely
Q What is the most important principle when applying design patterns in microservices?
A Patterns are tools, not rules. Apply them only when they solve a real pain point. The most common
mistake is over-engineering early — adding Saga, CQRS, and Event Sourcing to a system that could
be a simple CRUD app. Start with the simplest solution; add patterns when you hit the specific problem
they solve. Always articulate the trade-off: every pattern adds operational or cognitive complexity in
exchange for a specific benefit.
2. Decomposition Patterns
Decomposition patterns answer the first and most consequential question in microservices: how do you
break a system into services? Getting this wrong is expensive — poorly bounded services lead to chatty
inter-service communication, distributed monoliths, and teams that can't deploy independently.
E-commerce Example
Business Capability Service Owns Data Key Operations
Product Management Catalog Service Products, Categories, Add product, Update price,
Attributes Search
Order Management Order Service Orders, Order Items, Place order, Cancel, Track
Status
Shipping & Logistics Shipping Service Shipments, Carriers, Schedule, Track, Notify
Tracking
Reviews & Ratings Review Service Reviews, Ratings, Submit, Approve, Aggregate
Moderation
Core Domain Your unique competitive Build with highest Recommendation Engine,
advantage. Where you quality. Custom Pricing Algorithm, Search
win or lose. microservices. Ranking
Migration Steps
1. Identify a capability to extract — choose a well-defined, low-risk area first (e.g., email
notifications).
2. Build the new microservice in isolation with its own database and deploy it.
3. Add a routing layer (API Gateway or facade) in front of the monolith.
4. Route traffic for that capability to the new service, keeping the monolith as fallback.
5. Monitor, validate correctness, then remove the old code from the monolith.
6. Repeat for the next capability. The monolith 'shrinks' with each iteration.
Pros Cons
• Zero risk of big-bang failure • Routing layer adds complexity
• Can be done incrementally • Dual writes during transition period
• Monolith keeps running during migration • Data migration between old/new DB
• Learn as you go • Long-lived migration (months/years)
• Deploy new service independently • Team must maintain both systems
Q What is the Anti-Corruption Layer (ACL) pattern and when is it used with Strangler Fig?
A The ACL is a translation layer that sits between a new service and the legacy monolith (or external
system), preventing the legacy model's concepts from 'corrupting' the new service's clean domain
model. When extracting a service from a monolith, the new service often needs data that still lives in
the monolith. Instead of having the new service understand the monolith's schema or APIs directly, you
create an ACL that translates the monolith's concepts into the new service's ubiquitous language. For
example, the monolith might represent a customer with a flat 'CustomerRecord' struct; the ACL
translates this into a proper 'Customer' aggregate with value objects that the new service uses. This is
especially important when the monolith uses a different programming paradigm or has messy,
inconsistent naming.
3. Data Management Patterns
Data management is where most microservice projects encounter their hardest problems. The shift from
shared databases to service-owned data enables autonomy but introduces distributed consistency
challenges.
Product Catalog MongoDB (Document) Flexible schema for varied product attributes
Order Service PostgreSQL (Relational) ACID transactions, complex queries with joins
Q How do you handle queries that need data from multiple services (JOIN problem)?
A Several strategies depending on latency and consistency needs: (1) API Composition — the caller
(or an API Gateway/BFF) makes parallel requests to each service and aggregates results in memory.
Good for low-data-volume queries. (2) CQRS with a dedicated Query Service — publish events from
each service to a Query Service that maintains a pre-joined, denormalized read model in its own
database. Query the read model directly. High performance but eventually consistent. (3) GraphQL
Gateway — acts as a composition layer, resolving fields by calling individual services. (4) Event-driven
denormalization — Order Service subscribes to ProductUpdated events and stores a local copy of
product name/price in the order record (accept some duplication). The right choice depends on how
frequently the data changes and how stale you can tolerate it.
Choreography-Based Saga
Services react to events autonomously — no central coordinator. Each service knows what to do when it
receives a specific event.
// Order Service
@KafkaListener(topics = 'order-commands')
public void createOrder(CreateOrderCommand cmd) {
Order order = [Link](new Order(cmd, [Link]));
[Link](new OrderCreatedEvent([Link](), [Link]()));
}
Orchestration-Based Saga
A central Saga Orchestrator (a dedicated service or workflow engine) tells each service what to do,
tracks state, and handles failures.
@Override
public OrderResult placeOrder(PlaceOrderRequest req) {
String orderId = [Link](req);
String reservationId = null;
String paymentId = null;
try {
reservationId = [Link](orderId, [Link]());
paymentId = [Link](orderId, [Link](), [Link]());
[Link](orderId);
return [Link](orderId);
} catch (Exception e) {
// Compensate in reverse order
if (paymentId != null) [Link](paymentId);
if (reservationId != null) [Link](reservationId);
[Link](orderId, [Link]());
return [Link](orderId, [Link]());
}
}
}
Coupling Lower — services only know about Higher — orchestrator knows all steps
events
Visibility Hard to see the big picture Easy — orchestrator tracks full state
Testing Complex — must simulate full event Simpler — test orchestrator + each step
chain
Failure handling Each service handles its own failures Central failure + compensation logic
When to use Simple flows (2-3 steps), highly Complex multi-step flows, need visibility
decoupled teams
Q How does a Saga handle partial failures and ensure no data is lost?
A Each step in a Saga must be idempotent (safe to retry) and must have a defined compensating
transaction. Partial failure handling: (1) The step fails after DB write but before publishing an event —
use the Outbox Pattern: write event to an outbox table in the same transaction; a relay publishes it. (2)
The event is published but not consumed — message brokers guarantee at-least-once delivery;
consumers deduplicate by tracking processed event IDs. (3) A compensation fails — compensations
must also be idempotent and retried until success. In extreme cases (compensation also fails), use a
dead letter queue and trigger a manual resolution workflow with alerting. Tools like Temporal handle all
of this automatically: they persist workflow state durably and retry activities until they succeed, making
Sagas much more reliable.
@event_handler(OrderCreated)
def on_order_created(self, event: OrderCreated):
# Denormalized view — includes customer name, product names
[Link].insert_one({
'_id': event.order_id,
'customer_id': event.customer_id,
'customer_name': self._fetch_customer_name(event.customer_id),
'items': self._enrich_items([Link]),
'total': [Link],
'status': 'PENDING',
'created_at': [Link]()
})
@event_handler(OrderStatusChanged)
def on_status_changed(self, event: OrderStatusChanged):
[Link].update_one(
{'_id': event.order_id},
{'$set': {'status': event.new_status, 'updated_at': [Link]()}}
)
Benefits Challenges
• Complete audit trail for free • Querying current state requires replay (use
• Time-travel: query state at any past point snapshots)
• Natural integration with event-driven arch • Schema evolution is complex (immutable
history)
• Rebuild any read model from event history
• Not suitable for simple CRUD
• Debug by replaying what actually
happened • High event volume grows the store fast
• Learning curve for the team
Q How do snapshots work in Event Sourcing and when do you need them?
A As the event log grows (thousands of events per aggregate), replaying all events to get current
state becomes slow. Snapshots are periodic checkpoints: serialize the aggregate's current state into a
snapshot at a specific event version (e.g., every 100 events). On load, fetch the latest snapshot and
only replay events that occurred after it. Implementation: after saving, if event count % 100 == 0, write
a snapshot. On load, find the latest snapshot by aggregate ID and version, deserialize it, then stream
and apply only newer events. Snapshots don't replace the event log — the log remains the source of
truth. You need snapshots when: a) aggregate has many events (>500), b) rehydration latency
exceeds your SLA, c) the aggregate is read frequently (every user request). Most aggregates don't
need snapshots — only high-volume, frequently-accessed aggregates like bank accounts or shopping
carts.
3.5 Outbox Pattern
Solves the dual-write problem: ensuring a database write and a message broker publish are atomic.
Without this pattern, a crash between the two operations leaves the system inconsistent.
Q Why is the Outbox Pattern preferable to simply using a try-catch around DB write + event
publish?
A A try-catch approach has two failure scenarios that cannot be caught: (1) The DB write succeeds
but the process crashes before publishing — the event is lost forever. The catch block never runs
because the process is dead. (2) The DB write succeeds, publish succeeds, but the DB commit then
fails — you have a published event with no corresponding DB state. This is the 'dual-write problem'
and no amount of try-catch logic can solve it because it requires atomic operations across two different
systems (DB + broker) — which by definition are not atomic. The Outbox Pattern solves this by writing
to only one system (the database) atomically, then using a reliable relay (Debezium/CDC or polling
worker) to publish events. The relay guarantees at-least-once delivery; consumers handle
idempotency for any duplicates.
4. Communication Patterns
Authentication Validate JWT/OAuth tokens Verify signature, check expiry, extract claims
before forwarding
Rate Limiting Throttle requests per client/IP/API 100 req/min per user, 1000/min per tenant
key
SSL Termination Handle HTTPS, forward HTTP Reduce TLS overhead on each service
internally
Caching Return cached responses for GET Cache product catalog for 5 minutes
requests
Logging & Tracing Inject Trace-ID, log all requests Centralized access logs, OpenTelemetry
Q What are the trade-offs between a single API Gateway and multiple specialized gateways?
A A single gateway is simpler to operate and provides a consistent cross-cutting concern
implementation. However, it becomes a bottleneck if all teams must coordinate changes to it, and a
misconfiguration can take down all APIs simultaneously. Multiple gateways (per product line or per
client type — mobile, web, B2B) allow team autonomy and fault isolation but multiply operational
overhead. A practical middle ground: one gateway for external traffic (internet-facing), with separate
internal routing for service-to-service. For large organizations, consider a platform team that owns
gateway infrastructure but provides self-service configuration APIs so product teams can manage their
own routes without a central bottleneck.
Publish-Subscribe (Pub/Sub)
Producer publishes events to a topic. All subscribed consumers receive every event independently.
Producers and consumers are fully decoupled — neither knows about the other.
@KafkaListener(topics='order-events', groupId='notification-service')
public void onOrderCreatedSendEmail(OrderCreatedEvent event) {
[Link]([Link](), [Link]());
}
@KafkaListener(topics='order-events', groupId='analytics-service')
public void onOrderCreatedTrack(OrderCreatedEvent event) {
[Link]([Link](), [Link]());
}
[Link].basic_publish(
exchange='',
routing_key='pricing-requests',
body=[Link]({'product_id': product_id, 'quantity': quantity}),
properties=[Link](
reply_to=reply_queue,
correlation_id=correlation_id,
)
)
# Wait for response (with timeout)
response = self._wait_for_response(reply_queue, correlation_id, timeout=5.0)
return Decimal(response['price'])
Q How do you handle message ordering in Kafka and why does it matter?
A Kafka guarantees ordering within a partition, not across partitions. A topic with 10 partitions
processes messages in 10 parallel streams, each ordered internally. This matters when events must
be processed in sequence for the same entity — e.g., OrderCreated must be processed before
OrderShipped for the same orderId. Solution: use the entity ID as the Kafka message key. Kafka's
partitioner hashes the key and always routes messages with the same key to the same partition. All
events for orderId='ORD-123' land in partition 3 and are consumed in order. The trade-off: all events
for the same order are processed by one consumer, limiting parallelism per entity. However, different
orders (different keys) process fully in parallel across all partitions. For truly globally ordered events
(rare), use a single partition — but this limits throughput to one consumer.
5. Reliability Patterns
Distributed systems fail in ways monoliths don't: network timeouts, partial failures, cascading outages.
These patterns build resilience into your services.
CLOSED All calls pass through. Failures counted. Failure rate > threshold → OPEN
State Behavior Transition Trigger
OPEN All calls fail immediately (no network call). Wait duration elapsed → HALF-OPEN
Fallback returned.
HALF-OPEN Limited test calls allowed through. Tests succeed → CLOSED; Tests fail
→ OPEN
Q How do you choose the right threshold values for a Circuit Breaker?
A Start with the service's SLA and observed failure patterns. Sliding window size: enough calls to be
statistically meaningful — 20-50 for high-traffic services. Failure rate threshold: typically 50-60% —
lower than 50% may trip on normal transient errors; higher than 60% means too many failures before
tripping. Wait duration: should exceed the expected recovery time of the downstream service — if a
service typically restarts in 30 seconds, use a 30-60 second wait. Permitted calls in HALF-OPEN: 3-5
calls is enough to determine health without overwhelming a recovering service. Monitor circuit state
metrics (Prometheus gauges for state: 0=CLOSED, 1=OPEN, 2=HALF-OPEN) and tune based on
production data. Also consider slow calls — a circuit should trip on high latency as well as errors, since
a slow service can exhaust thread pools just as badly as a failing one.
@Bean
public ThreadPoolBulkhead paymentBulkhead() {
ThreadPoolBulkheadConfig config = [Link]()
.maxThreadPoolSize(10) // Completely separate pool for payments
.coreThreadPoolSize(5)
.queueCapacity(20)
.build();
return [Link]().bulkhead('payment', config);
}
class OrderService:
def place_order(self, request: PlaceOrderRequest) -> str:
with tracer.start_as_current_span('place-order') as span:
span.set_attribute('[Link]', request.customer_id)
span.set_attribute('[Link]', float([Link]))
# Child span for downstream call (traceId propagated via HTTP headers)
with tracer.start_as_current_span('[Link]-service'):
self.inventory_client.reserve([Link], [Link])
span.set_attribute('[Link]', [Link])
return [Link]
@PostMapping("/orders")
public ResponseEntity<OrderResponse> placeOrder(@RequestBody PlaceOrderRequest req) {
// traceId injected by OpenTelemetry auto-instrumentation into MDC
// [Link]("traceId") → "abc123xyz"
[Link]("Placing order",
kv("event", "[Link]"),
kv("customerId", [Link]()),
kv("itemCount", [Link]().size()),
kv("total", [Link]())
);
// Outputs:
// {"timestamp":"2024-01-15T10:30:00Z","level":"INFO","service":"order-service",
// "traceId":"abc123xyz","spanId":"def456","event":"[Link]",
// "customerId":"USR-789","itemCount":3,"total":149.99}
}
}
Q What is the difference between logging, metrics, and tracing and when do you use each?
A They answer different questions: Logs answer 'What happened?' — discrete events with full context
(error messages, request details). Use when debugging a specific incident or auditing what occurred.
Query by traceId to see all events for one request. Metrics answer 'How is the system performing?' —
numeric measurements aggregated over time (request rate, error %, latency percentiles, CPU). Use
for monitoring health, capacity planning, alerting, and SLA reporting. Dashboards show trends; alerts
fire when thresholds are crossed. Traces answer 'Where did the time go?' — a request's journey
across services with timing for each step. Use when debugging latency issues or understanding which
service in a call chain is slow. All three must be correlated by a common trace ID. The modern
standard is OpenTelemetry, which provides a unified API to emit all three signals with automatic
correlation.
7. Deployment Patterns
Client-Side Service registers with Netflix Eureka + Pro: Client controls LB algo. Con:
registry. Client queries Ribbon Each client needs discovery
registry, picks instance, library.
calls directly.
Server-Side Client calls load balancer. AWS ELB + Route Pro: Client is simple. Con: LB can
LB queries registry and 53, k8s Service + be a bottleneck/SPOF.
routes. kube-proxy
DNS-Based Services registered as Consul DNS, k8s Pro: Works with any
DNS SRV records. Client CoreDNS language/framework. Con: DNS
resolves via DNS. caching delays updates.
Service Mesh Envoy sidecar handles Istio + Envoy Pro: Fully transparent. Con:
discovery. Control plane Significant operational
(Istio) manages registry. complexity.
Q How does Kubernetes handle service discovery and load balancing internally?
A Kubernetes provides built-in service discovery via DNS and kube-proxy. When you create a Service
resource (kind: Service), Kubernetes: (1) Assigns a stable virtual IP (ClusterIP). (2) Creates a DNS
entry: [Link] resolving to the ClusterIP. (3) kube-proxy (running on
every node) watches the API server for Endpoint changes and programs iptables or IPVS rules to
route ClusterIP traffic to one of the healthy pod IPs (round-robin by default). When a pod starts, it's
added to Endpoints; when it fails liveness/readiness probes, it's removed — automatically removing it
from the load balancer without any service discovery library in your code. For more advanced LB (least
connections, consistent hashing, circuit breaking), use a service mesh like Istio that replaces kube-
proxy's simple round-robin with Envoy's rich traffic management.
8. Master Interview Q&A Bank
These are the most frequently asked, highest-signal questions at senior/architect-level microservices
interviews at FAANG and unicorn companies. Each answer demonstrates depth, trade-off awareness,
and real-world experience.
Q How would you break apart a 10-year-old e-commerce monolith step by step?
A Step 1: Instrument the monolith — add distributed tracing, profiling, and module-level metrics to
understand coupling and hot paths. Step 2: Identify seams using Event Storming — map domain
events and find bounded contexts. Step 3: Prioritize extraction by: highest value (frequently changed
code), lowest risk (fewest dependencies), fastest feedback. Notification service is usually first — few
inbound dependencies, mostly outbound. Step 4: Extract using Strangler Fig — build the service, add
routing layer, migrate data, validate, remove from monolith. Step 5: Handle data — use dual writes
(write to both monolith DB and new service DB via Outbox) during migration, then cut over. Step 6:
Address cross-cutting concerns — shared auth, logging, tracing must be established early and each
service adopts them. Timeline: plan for 18-36 months for a large monolith. Never do a big-bang
rewrite.
Q What is the CAP theorem and how does it affect your microservices design choices?
A CAP theorem states that a distributed system can guarantee at most two of: Consistency (every
read sees the latest write), Availability (every request gets a response), Partition Tolerance (system
works despite network failures). Since network partitions are unavoidable in distributed systems, you
must choose between CP (consistency + partition tolerance) or AP (availability + partition tolerance).
CP systems (e.g., HBase, etcd, Zookeeper) refuse to serve potentially stale data during a partition —
correct data or no data. AP systems (e.g., Cassandra, DynamoDB, CouchDB) continue serving
potentially stale data during a partition — always available but may be inconsistent. In microservices,
most user-facing services should be AP (high availability, eventual consistency) because downtime is
worse than temporary staleness. Financial systems requiring strict accuracy should be CP. Design
accordingly: use AP databases for product catalog and user sessions; use CP for payment ledgers
and inventory reservations.
8.2 Data & Consistency Deep Dives
Q Explain exactly how you would implement the Saga pattern for a flight booking system.
A Services involved: Booking Service (orchestrator), Flight Inventory Service, Seat Reservation
Service, Payment Service, Notification Service. Using orchestration (Temporal workflow): Step 1 —
Booking Service creates booking record (PENDING) and starts saga. Step 2 — Reserve flight
inventory (check availability, hold seat count for 10 minutes). Compensation: release hold. Step 3 —
Reserve specific seat (assign seat to passenger). Compensation: unreserve seat. Step 4 — Charge
payment (process card). Compensation: refund. Step 5 — Confirm booking (CONFIRMED). Step 6 —
Send confirmation email (fire-and-forget, no compensation needed). Each activity is idempotent: flight
inventory uses a reservationId for deduplication; payment uses an idempotency key. If Step 4
(payment) fails, Temporal automatically executes: unreserve seat (Step 3 compensation), release flight
hold (Step 2 compensation), cancel booking. The workflow is durable — if the process crashes mid-
saga, Temporal replays the workflow from the last completed step.
Q How do you implement distributed rate limiting across multiple instances of an API
Gateway?
A Single-instance rate limiting (in-memory counters) doesn't work across multiple gateway instances.
Distributed rate limiting requires a shared state store. Implementation with Redis: use a sliding window
counter algorithm. For each request, increment a counter in Redis with a key of
user_id:window_timestamp (e.g., 1-second windows). Use Redis INCR + EXPIRE atomically (via Lua
script or Redis pipeline). If counter > limit, return 429. Redis can handle 100K+ operations/second with
sub-millisecond latency, making this viable even for high-traffic gateways. More sophisticated: token
bucket algorithm with Redis — store remaining tokens and last refill time per user. Use the Lua script
for atomic check-and-consume. For extreme scale, use local rate limiting with a small burst allowance
per instance plus Redis for global rate limiting — check local first (fast path), sync to Redis periodically.
Libraries: redis-rate-limiter, Envoy's built-in rate limiting (calls a gRPC rate limit service).
Q How do you ensure data consistency when a service needs to roll back due to a failed Saga
while some compensating transactions also fail?
A This is the 'non-atomic compensation' problem. Defense-in-depth approach: (1) Design
compensations to be idempotent and retryable — they should always eventually succeed given
enough retries. Use exponential backoff with very long retry horizons (hours, not seconds) for
compensations. (2) Use a reliable workflow engine (Temporal, AWS Step Functions) that durably
persists workflow state — even if your process crashes, compensation retries continue after restart. (3)
For compensations that truly cannot be automated (e.g., a refund to a closed card), implement a dead
letter queue (DLQ) that triggers a manual review workflow with operator notification and tooling to
perform the action manually. (4) Accept that some states require human intervention — design your
system to handle 'stuck' orders gracefully (show them in an admin UI, alert on-call). (5) Implement a
reconciliation job that periodically audits saga-in-progress records and alerts/resolves ones that are
stuck beyond a time threshold. Perfect automated compensation isn't always achievable; the system
must degrade gracefully to manual processes.
Q What are the key considerations when choosing between REST and event-driven
communication for a new microservice integration?
A Use synchronous REST when: the client needs an immediate response to continue its work (user-
facing request/response), strong consistency is required (must know the result before proceeding), the
operation is naturally request/reply in nature (query a resource, submit a form). Use event-driven
async when: the operation can complete in the background (sending an email, generating a report,
updating a search index), you need to fan out to multiple consumers (audit logging, analytics, caching),
you want to decouple services so they can evolve independently, or the downstream service may be
slow/unavailable and you can't hold the user waiting. A practical guide: if the user is waiting for the
response → REST. If the user doesn't need to wait (fire and forget from user's perspective) → events.
If the system needs to coordinate multiple services atomically → events + Saga. Remember: REST
calls can be made more resilient with retry/circuit breaker but still couple services temporally. Events
decouple services but introduce eventual consistency. Most real-world systems use both — REST for
queries and user-facing writes, events for notifications, projections, and cross-domain workflows.
Q What is the difference between horizontal and vertical scaling and when do microservices
help?
A Vertical scaling (scale up): add more CPU/RAM to the same server. Simple but has hardware limits
and creates a single point of failure. Horizontal scaling (scale out): add more instances of the same
service. Microservices enable fine-grained horizontal scaling — scale only the services that are under
load. In a monolith, you must scale everything together (wasteful). With microservices, if Order Service
is the bottleneck during a sale, scale it from 3 to 30 instances without touching Payment or Catalog
services. Kubernetes makes this automatic with Horizontal Pod Autoscaler (HPA) based on
CPU/memory/custom metrics.
Q How does a service mesh provide mTLS without changing application code?
A The service mesh uses the sidecar pattern: an Envoy proxy is injected into every pod as a sidecar
container. iptables rules (set up by an init container) redirect all inbound and outbound traffic through
Envoy. The application thinks it's making a plain HTTP call to localhost; Envoy intercepts it, performs
the mTLS handshake with the destination's Envoy sidecar, and sends the request over an encrypted
tunnel. The destination Envoy decrypts it and forwards to the local application container — again over
plain HTTP on localhost. From the application's perspective, zero encryption code. The Istio control
plane (istiod) acts as a Certificate Authority, issuing short-lived certificates (e.g., 24-hour SVID —
SPIFFE Verifiable Identity Document) to each Envoy. Certificates rotate automatically. Authorization
policies (which service identity can call which service's endpoint) are also enforced by Envoy
transparently.
9. Pattern Quick Reference
How to split the application Decompose by Business Autonomy vs. cross-service communication
Capability / DDD
Subdomain
Services sharing a database Database per Service + Data duplication, no joins, eventual
API access consistency
Full audit trail required Event Sourcing Complex querying, schema evolution
difficulty
Atomic DB write + event Outbox Pattern Additional table + relay process to operate
publish
Single entry point for clients API Gateway Single point of failure, must be highly
available
Different API shape per client Backend for Frontend Multiple backends to maintain
type (BFF)
Cascading failures from slow Circuit Breaker Requires tuning; false positives trip
service unnecessarily
Resource exhaustion from Bulkhead Thread pool overhead; over-partitioning
one consumer wastes threads
Transient failures on calls Retry + Exponential Only for idempotent ops; adds latency on
Backoff + Jitter failure
Safe gradual rollout with Canary Deployment Traffic splitting infrastructure required
monitoring
Debugging across service Distributed Tracing Instrumentation effort, trace storage cost
Problem You Face Pattern to Apply Key Trade-off
boundaries (OpenTelemetry)
Shared Database Multiple services read/write the Database per service + API access
same DB tables
Chatty Services Service A calls B, B calls C, C calls Consolidate services or use async
D synchronously for every request events
Distributed Monolith Services are deployed separately Enforce API contracts, separate
but coupled by shared DB/tight API databases
contracts
God Service One service owns too many Decompose using DDD bounded
business capabilities contexts
Synchronous Saga Saga steps use synchronous REST Use async messaging or workflow
calls instead of async events engine
No Idempotency Retried POST requests create Add idempotency keys to all write APIs
duplicate records/charges
Skipping Outbox DB write + direct broker publish Use Outbox Pattern with CDC relay
without transaction
Premature Splitting into microservices before Start modular monolith; extract when
Decomposition domain is understood pain is real