16 questions on microservices: service boundaries, database per service, sagas, the transactional outbox, idempotent consumers, circuit breakers, and the dual-write problem.
01MicroservicesFoundation
When to use microservices
Answer
Split into microservices when teams need independent deploy cycles and clear ownership of business capabilities.
You pay with network latency, partial failures, distributed transactions problems, and heavier operations.
A modular monolith with clear boundaries is often enough until those boundaries are proven under real change pressure.
Splitting only by technical layer (UI service, DB service) rarely helps. Prefer business capability cuts.
Need independent scale for one hotspot (for example image processing) can justify extracting that piece first.
If one release train and one small team ships everything, microservices overhead usually hurts more than it helps.
Regulatory or data isolation requirements sometimes force separate services and stores.
Start with fewer services. Split further when a module has different scale, risk, or ownership needs.
Interviews expect you to name tradeoffs, not to declare microservices always better.
Tooling (CI/CD, observability, container orchestration) must exist before a fleet of services stays healthy.
Flow diagram: Split only when independence pays off
C#
// One deployable, clear modulespublicsealedclassBillingModule { }
publicsealedclassShippingModule { }
// Extract a service when deploy cadence or scale truly diverges.// public sealed class ShippingServiceHost { }
02MicroservicesFoundation
Service boundaries
Answer
Draw boundaries around business capabilities or bounded contexts, not around individual tables.
A service owns its data and logic. Other services use its API or consume its published events.
Chatty cross-service calls for one user action mean the cut is wrong or a coarser API is missing.
Start coarse. Split when a part has clear ownership, a different scale profile, or a separate change rate.
Shared libraries for DTOs can create hidden coupling. Prefer versioned contracts and tolerant readers.
Ask which team on-calls the service and who can deploy it. Ownership is part of the boundary.
Data that changes together often belongs together. Data that changes for different reasons often belongs apart.
Avoid distributed monoliths: many services that must deploy in lockstep because contracts are brittle.
Domain events mark facts that other contexts may react to without sharing the writer database.
Review boundaries with real user journeys. If one click always needs five sync hops, redesign.
Flow diagram: Capability boundary owns its data
C#
// Orders owns order data; it does not join Customer tables from another DBpublicinterfaceIOrderService
{
Task<OrderId> PlaceAsync(PlaceOrderCommand cmd, CancellationToken ct);
}
// Need customer display data? Call Customers (or use a local projection),// do not SELECT across another service database.
03MicroservicesFoundation
Database per service
Answer
Each service owns its persistence. No shared tables or cross-service foreign keys in one database.
Share data through APIs or published events, not by reading another service database.
Duplicated data is allowed when it is a deliberate read model with a clear source of truth and update path.
Cross-service joins move to composition at the API layer, to a BFF, or to projections built from events.
Schema changes stay local. You avoid multi-team migrations on one shared schema.
Transactions do not span services. Use sagas/compensations or accept eventual consistency for multi-service workflows.
Reporting across services often needs a separate read store fed by events or ETL, not live cross-DB joins.
Polyglot persistence is optional: each service may choose SQL, document, or blob storage for its needs.
Breaking this rule "just for this report" recreates a distributed monolith through the back door.
Migration from a shared DB: carve schemas first, then separate databases, then separate deployables.
Flow diagram: No shared tables across services
C#
// Service Aawait dbA.Orders.AddAsync(order);
await dbA.SaveChangesAsync();
await bus.Publish(newOrderPlaced(order.Id));
// Service B keeps its own projection; it does not SELECT from DB A
04MicroservicesFoundation
API gateway
Answer
An API gateway is a single entry for clients: routing, TLS termination, authentication, rate limits, and sometimes aggregation.
It keeps mobile and web clients from knowing dozens of internal service URLs.
Keep business rules out of the gateway. It should stay a thin edge, not a second monolith.
BFF (backend for frontend) is a gateway shape tuned to one client type (web vs mobile) with screen-shaped responses.
Gateways centralize cross-cutting policies so every service does not reimplement them differently.
Failure of the gateway is a single point of failure. Run it with the same HA care as any edge tier.
Aggregation in the gateway can hide chatty backends, but heavy orchestration belongs in a dedicated service or BFF.
YARP, Azure API Management, Kong, and similar products are common implementations. Pick based on ops skill and features.
Internal service-to-service traffic often bypasses the public gateway and uses a mesh or private DNS.
Versioning, request logging, and correlation ids are often applied once at the gateway.
In a microservices architecture with a database-per-service pattern, traditional two-phase commit (2PC) distributed transactions are impractical due to high lock contention, network latency, and availability degradation.
A Saga is a sequence of local transactions spanning multiple services. Each local transaction updates data within a single service and emits an event or message triggering the next step.
Compensating Transactions: If a step fails business validation (such as insufficient customer credit), the saga executes a series of compensating transactions in reverse order to undo the changes made by preceding steps, restoring eventual data consistency.
Flow diagram: Saga execution and compensation rollback workflow
Choreography (Event-Driven): Services listen to domain events published to a broker and autonomously trigger their local transactions. Pros: simple to set up with no centralized bottleneck. Cons: difficult to track overall state, test end-to-end, and debug cyclical dependencies as workflows grow.
Orchestration (Coordinator-Driven): A centralized orchestrator (e.g. MassTransit Courier, Temporal, Azure Durable Functions) explicitly sends commands to participant services and awaits status replies. Pros: centralized workflow visibility, simplified compensation routing. Cons: extra orchestrator service to maintain.
12MicroservicesIntermediate
Transactional Outbox Pattern
Answer
The Dual-Write Problem: When a service updates its database and publishes a message to a message broker (such as RabbitMQ or Kafka), doing both in separate operations is prone to partial failure. If the database commit succeeds but the message broker publish fails, downstream systems never receive the event. If the message publish succeeds but the database transaction rolls back, downstream systems process phantom data.
The Solution: Save the outgoing message into an Outbox table inside the same local database transaction that commits the business entity update.
Asynchronous Relay: A background worker (using a polling publisher or Change Data Capture / Debezium) reads unpublished records from the Outbox table, publishes them to the message broker, and marks them as processed.
At-Least-Once Delivery: Because network timeouts can occur while acknowledging published messages, the relay guarantees at-least-once delivery. Downstream consumers must be designed to be idempotent.
Flow diagram: Transactional outbox workflow
C# (Transactional Outbox with EF Core)
publicasyncTaskPlaceOrderAsync(CreateOrderDto dto, AppDbContext db, CancellationToken ct)
{
awaitusingvar transaction = await db.Database.BeginTransactionAsync(ct);
var order = newOrder { Id = Guid.NewGuid(), Total = dto.Total };
db.Orders.Add(order);
var outboxMessage = newOutboxMessage
{
Id = Guid.NewGuid(),
OccurredOnUtc = DateTime.UtcNow,
Type = "OrderCreatedEvent",
Payload = JsonSerializer.Serialize(newOrderCreatedEvent(order.Id, order.Total)),
ProcessedOnUtc = null
};
db.OutboxMessages.Add(outboxMessage);
// Commits business state AND message payload atomicallyawait db.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);
}
13MicroservicesIntermediate
Outbox vs Saga
Answer
Outbox solves dual write inside one service: DB state and "message to publish" commit together.
You save business data and an outbox row in one transaction. A relay publishes later to the broker.
A saga coordinates multiple steps across services (or long waits) without a distributed ACID transaction.
Saga steps usually need compensating actions when a later step fails.
Use an outbox whenever you publish after a DB commit. Use a saga when the workflow spans several services or long waits.
They combine often: each saga participant uses an outbox so its local commit and outgoing message stay aligned.
Outbox alone does not roll back other services. Saga alone does not fix dual write inside a participant.
Interview answer: outbox is a reliability pattern for messaging; saga is a workflow pattern for consistency across boundaries.
Choreographed sagas often rely on events that were published via outbox from each service.
Pick the smallest tool that matches the failure mode you are fixing.
Flow diagram: Outbox is local consistency; saga is multi-step workflow
C#
// Outbox: same transaction
_db.Orders.Add(order);
_db.Outbox.Add(newOutboxMessage("OrderPlaced", payload));
await _db.SaveChangesAsync();
// Saga: orchestrate steps across services with compensations// ReserveStock -> ChargeCard -> ConfirmOrder// On failure after charge: Refund + ReleaseStock
14MicroservicesIntermediate
Circuit Breaker Pattern in Microservices
Answer
Cascading Failure Prevention: In a distributed system, when a downstream service becomes slow or unresponsive, upstream callers continuing to send requests will quickly exhaust their own thread pools and connection sockets, causing a domino failure across the entire application.
The Circuit Breaker pattern wraps remote calls in a stateful monitor that detects recurring failures and stops dispatching calls to the failing downstream service.
The Three Circuit States: Closed (normal execution, failures are counted), Open (requests fail immediately without making a network call, returning a cached response or fallback), and Half-Open (a limited number of trial requests are sent through to test whether the downstream dependency has recovered).
State diagram: Circuit breaker state transitions
C# (Polly Circuit Breaker in .NET)
services.AddHttpClient<IInventoryClient, InventoryClient>()
.AddTransientHttpErrorPolicy(policy =>
policy.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (outcome, timespan) => Log.Warning("Circuit OPEN for 30s due to downstream errors"),
onReset: () => Log.Information("Circuit CLOSED: Normal execution resumed"),
onHalfOpen: () => Log.Information("Circuit HALF-OPEN: Testing downstream health")
));
15MicroservicesAdvanced
Advanced: dual-write problem
Answer
Dual write means updating the database and publishing a message as two separate commits. Either can succeed while the other fails.
Save-then-publish can leave a committed row with no event. Downstream never reacts.
Publish-then-save can leave an event for a row that never committed. Downstream acts on fiction.
Transactional outbox writes the business row and an outbox row in one DB transaction, then a relay publishes.
Change data capture from the transaction log is another way to derive events from committed state.
Consumers still need idempotency because brokers usually deliver at least once.
Distributed transactions (2PC) across DB and broker are rarely the answer in microservices.
The dual-write problem appears anywhere you update two systems without one atomic boundary (DB + search index, DB + cache).
For cache, prefer cache-aside invalidation patterns and accept TTL repair, or update cache in ways that tolerate lag.
Name the failure mode in design reviews: which system is source of truth if the second write fails?
Flow diagram: Two writes, two failure modes
C#
// Fragile dual writeawait db.Orders.AddAsync(order);
await db.SaveChangesAsync();
await bus.PublishAsync(newOrderPlaced(order.Id)); // may fail after commit// Outbox: same transactionawait db.Orders.AddAsync(order);
await db.Outbox.AddAsync(newOutboxMessage("OrderPlaced", payload));
await db.SaveChangesAsync();
16MicroservicesAdvanced
Advanced: chatty service boundaries
Answer
Five synchronous hops for one click usually means a bad service cut or a missing coarser API (BFF / screen-shaped read).
Latency stacks additively. p99 across hops multiplies user pain.
Failure on hop three leaves earlier hops needing undo or compensation logic.
Ask one service for the use-case payload instead of stitching many CRUD calls in the client.
Async events help when the UI can wait for eventual consistency. They do not fix a chatty sync graph by themselves.
A modular monolith with in-process calls may be healthier than chatty remote calls pretending to be microservices.
Measure: if one page needs N service calls every time, treat that as a design smell with metrics.
API composition in a BFF can hide chattiness from the browser but still loads the backend. Prefer co-locating hot data.
Ownership fights ("that field belongs to them") sometimes force chattiness. Revisit the boundary.
Caching aggregated reads can mask chattiness temporarily but adds stale-data complexity.
Flow diagram: Too many sync hops
C#
// Chattyvar customer = await customers.GetAsync(id);
var orders = await ordersApi.ListAsync(id);
var flags = await flagsApi.GetAsync(id);
// Coarser read model / BFFvar page = await accountPage.GetAsync(id); // one call shaped for the screen