Microservices interview questions

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 modules public sealed class BillingModule { } public sealed class ShippingModule { } // 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 DB public interface IOrderService { 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 A await dbA.Orders.AddAsync(order); await dbA.SaveChangesAsync(); await bus.Publish(new OrderPlaced(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.
Flow diagram: Single entry in front of services
C#
// YARP-style reverse proxy routes (sketch) var builder = WebApplication.CreateBuilder(args); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); // appsettings: // /api/orders/{**catch-all} -> https://orders // /api/catalog/{**catch-all} -> https://catalog var app = builder.Build(); app.MapReverseProxy(); app.Run();
05MicroservicesFoundation

Service discovery

Answer

  • Clients and gateways need current addresses as instances scale up, down, or move across hosts.
  • Kubernetes Services give stable DNS names and load-balance to pod endpoints. Prefer that when you run on Kubernetes.
  • Service meshes (sidecar proxies) can add discovery, mTLS, and traffic policy without app code changes.
  • Classic registries (Consul, Eureka) still appear in older platforms. Prefer platform primitives when they exist.
  • Client-side discovery: the client queries the registry and picks an instance. Server-side discovery: a load balancer does it.
  • Caches of discovery data go stale. Combine with health checks and short TTLs.
  • Hard-coded hostnames in config break as soon as you scale beyond one instance.
  • DNS TTLs and connection pooling can keep traffic on dead instances briefly. Use readiness probes and sane timeouts.
  • In .NET, HttpClient with platform DNS and Kubernetes service names is often enough inside the cluster.
  • Do not build a custom registry until you know the platform option cannot meet the need.
Flow diagram: Find healthy instances by name
C#
// Prefer platform DNS names over hardcoded IPs var client = httpClientFactory.CreateClient("orders"); // BaseAddress: https://orders.default.svc.cluster.local/
06MicroservicesFoundation

NServiceBus

Answer

  • NServiceBus is a .NET messaging framework from Particular Software that sits on brokers such as RabbitMQ, Azure Service Bus, SQS, and SQL Server.
  • You write message contracts and handler classes. The framework owns routing, serialization, retries, and the receive pipeline.
  • First-class sagas store saga data and correlate messages for long-running processes.
  • The outbox feature aligns database updates with message publish inside the NServiceBus pipeline.
  • Pub/sub and request/response patterns are built in, so you avoid hand-rolled glue for every endpoint.
  • Immediate and delayed retries, plus an error queue, are part of the operational model.
  • Compared with a thin queue client, you get sagas, outbox, and retries as one pipeline instead of separate libraries.
  • Endpoints are independently deployable processes or hosted inside a web host, depending on your topology.
  • Message versioning and polymorphism are supported with care. Prefer additive contract changes.
  • Licensing and ops (ServicePulse, ServiceControl) matter in production planning, not only the NuGet package.
Flow diagram: Handlers on top of a transport
C#
public sealed class OrderPlacedHandler : IHandleMessages<OrderPlaced> { public Task Handle(OrderPlaced message, IMessageHandlerContext context) { // business reaction; retries/pipeline come from NServiceBus return Task.CompletedTask; } } public sealed class OrderPlaced { public Guid OrderId { get; init; } }
07MicroservicesFoundation

Idempotent consumers

Answer

  • Brokers commonly deliver at-least-once. The same message can be processed more than once after retries or failovers.
  • Store processed message ids (inbox table) and skip duplicates, or design updates to be naturally idempotent (upsert by business key).
  • Do not rely on "it should only arrive once" as a correctness argument.
  • Side effects (charge card, send email) need explicit deduplication or an idempotency key from the producer.
  • Inbox rows should be written in the same transaction as the business update when possible.
  • Handler code that does read-modify-write without a unique constraint can double-apply under concurrency.
  • See also HTTP idempotency keys under system design for client retry patterns on APIs.
  • Poison messages that always fail should go to a dead-letter queue after limited retries, not infinite loops.
  • Logging the message id on every attempt makes duplicate diagnosis possible.
  • Exactly-once end-to-end is a property of the whole pipeline (inbox + side effects), not a broker checkbox alone.
Flow diagram: Idempotent consumer
C#
if (await db.ProcessedMessages.AnyAsync(x => x.Id == messageId)) return; // already handled await db.ProcessedMessages.AddAsync(new ProcessedMessage(messageId)); // apply business change await db.SaveChangesAsync();
08MicroservicesFoundation

Contracts and versioning

Answer

  • Services couple on contracts (HTTP payloads, event schemas), not on shared databases.
  • Prefer additive changes: new optional fields. Avoid removing or renaming fields without a migration plan.
  • Event consumers should ignore unknown fields when possible (tolerant reader).
  • Explicit versions (URI /v2, header, or event type name) help when breaking changes are unavoidable.
  • Publish both old and new event shapes for a transition window when many consumers exist.
  • Consumer-driven contract tests catch breaking changes before release.
  • Document deprecation and sunset dates. Track traffic on old versions with metrics.
  • Shared "fat" event models that grow forever become unmaintainable. Prefer focused event types per fact.
  • Binary formats need the same discipline as JSON. Field numbers in protobuf are a form of versioning.
  • Breaking a contract without a version forces lockstep deploys across teams.
Flow diagram: Evolve payloads without breaking consumers
C#
public sealed class OrderPlacedV1 { public Guid OrderId { get; init; } } public sealed class OrderPlacedV2 { public Guid OrderId { get; init; } public string? Source { get; init; } // additive }
09MicroservicesFoundation

Health and readiness

Answer

  • Liveness: the process is running. Orchestrators restart the container when liveness fails.
  • Readiness: the process can accept traffic (warmed, migrations done, critical dependency reachable as you define it).
  • Kubernetes uses these probes to route traffic (readiness) or replace instances (liveness).
  • Do not make liveness depend on every downstream. A dependency blip will restart healthy processes in a loop.
  • ASP.NET Core health checks (AddHealthChecks, MapHealthChecks) expose /health endpoints for probes.
  • Separate endpoints or tags for live vs ready so probes do not share one ambiguous signal.
  • Readiness may check DB connectivity. Keep checks cheap and timed out so probes do not overload the DB.
  • During startup, fail readiness until the app is warm. Keep liveness passing if the process is alive but still starting (or use startup probes).
  • Custom checks implement IHealthCheck. Register them with clear names for status payloads.
  • Gateways and load balancers should honor readiness so draining instances stop receiving work.
Flow diagram: Liveness vs readiness probes
C#
builder.Services.AddHealthChecks() .AddSqlServer(builder.Configuration.GetConnectionString("Main")!); var app = builder.Build(); app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false, // process only }); app.MapHealthChecks("/health/ready");
10MicroservicesIntermediate

Sync vs async communication

Answer

  • Synchronous HTTP/gRPC: the caller waits for a response. Simple to reason about, coupled in time.
  • Asynchronous messaging: the producer sends a message and continues. Consumers scale and fail independently.
  • Prefer async workflows when the user can tolerate eventual consistency (email sent, inventory adjusted later).
  • Use sync when the user needs an immediate answer from that dependency (price quote, auth decision).
  • Sync chains amplify latency and failure: if service C is down, A and B fail the request.
  • Async needs idempotent consumers, monitoring of lag, and clear ownership of dead letters.
  • gRPC suits low-latency internal sync calls. Events suit fan-out and decoupling.
  • Timeouts and retries on sync calls need budgets. Blind retries can stampede a sick dependency.
  • Many systems mix both: sync for the user-facing command ack, async for side effects.
  • Do not call sync in a loop across many services for one page. Aggregate or precompute instead.
Flow diagram: Wait for response vs fire and react later
C#
// Sync var stock = await inventoryHttp.GetFromJsonAsync<Stock>($"/stock/{sku}"); // Async await bus.PublishAsync(new ReserveStock(sku, qty));
11MicroservicesIntermediate

Saga Pattern for Distributed Transactions

Answer

  • 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
C# (Saga Orchestrator State Flow)
public async Task HandleOrderSagaAsync(OrderSagaData saga, IPublishEndpoint bus) { switch (saga.CurrentState) { case SagaState.OrderCreated: await bus.Publish(new AuthorizePaymentCommand(saga.OrderId, saga.Amount)); break; case SagaState.PaymentAuthorized: await bus.Publish(new ReserveInventoryCommand(saga.OrderId, saga.Items)); break; case SagaState.InventoryFailed: // Execute compensating transaction await bus.Publish(new RefundPaymentCommand(saga.OrderId, saga.Amount)); saga.CurrentState = SagaState.Compensating; break; } }

Choreography vs Orchestration

  • 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)
public async Task PlaceOrderAsync(CreateOrderDto dto, AppDbContext db, CancellationToken ct) { await using var transaction = await db.Database.BeginTransactionAsync(ct); var order = new Order { Id = Guid.NewGuid(), Total = dto.Total }; db.Orders.Add(order); var outboxMessage = new OutboxMessage { Id = Guid.NewGuid(), OccurredOnUtc = DateTime.UtcNow, Type = "OrderCreatedEvent", Payload = JsonSerializer.Serialize(new OrderCreatedEvent(order.Id, order.Total)), ProcessedOnUtc = null }; db.OutboxMessages.Add(outboxMessage); // Commits business state AND message payload atomically await 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(new OutboxMessage("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 write await db.Orders.AddAsync(order); await db.SaveChangesAsync(); await bus.PublishAsync(new OrderPlaced(order.Id)); // may fail after commit // Outbox: same transaction await db.Orders.AddAsync(order); await db.Outbox.AddAsync(new OutboxMessage("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#
// Chatty var customer = await customers.GetAsync(id); var orders = await ordersApi.ListAsync(id); var flags = await flagsApi.GetAsync(id); // Coarser read model / BFF var page = await accountPage.GetAsync(id); // one call shaped for the screen
Sources and further reading