Junior microservices interviews ask "what is a microservice." Senior interviews ask why your services share a database (so they're really one service that can't deploy independently), what happens when the payment call times out halfway through an order, and how you debug a request that touched six services at 3am. Microservices trade in-process simplicity for distributed-systems hard problems — and seniority is knowing which problems you just bought and how to pay for them.
This is 30 questions across four blocks: boundaries & decomposition, data & consistency, resilience & communication, and observability & production. Every answer is a short answer you could say in one breath, a deep dive into the mechanism with code, and the senior signal the interviewer is listening for. Every example is from Mattrx — a multi-tenant marketing-analytics platform on .NET 9 whose services talk over Kafka, Azure Service Bus, and Event Grid.
The lens for all 30
A junior lists patterns; a senior reasons about what you traded for them. "Should we use microservices?" gets "yes, they scale!" from a junior and "only when a module needs independent scaling/deploy/ownership — otherwise a modular monolith, because you're buying network failure, eventual consistency, and distributed debugging" from a senior.
Block A highlight — the distributed monolith
The biggest failure mode is going distributed too early: services that must deploy together, call each other in long synchronous chains, and share a DB — all the ops pain of microservices with none of the independence. The litmus test is one question: "can I deploy this service alone?" If releasing these three services needs a specific order, they're one service wearing three uniforms. Draw boundaries by business capability / DDD bounded context, not by technical layer (a layer split makes every feature cross every service).
Block B highlight — the outbox pattern
The dual-write problem: you update your DB and publish an event as two operations — a crash in between loses one. The outbox writes the event into an outbox table in the same transaction as the state change; a relay publishes it afterward.
// BUG (dual write): a crash between these two loses the event.
await _db.SaveChangesAsync(ct);
await _bus.PublishAsync(new CampaignPublished(id), ct); // if this fails, event is lost forever
// FIX (outbox): event + state commit atomically; a relay publishes from the outbox later.
using var tx = await _db.BeginTransactionAsync(ct);
_db.Campaigns.Update(campaign);
_db.Outbox.Add(new OutboxMessage(Guid.NewGuid(), "campaign.published", Serialize(evt)));
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct); // atomic: state change AND event persist together
And because delivery is at-least-once, every consumer must be idempotent — dedup by a stable event id:
public async Task Consume(ConsumeContext<ConversionTracked> ctx)
{
if (!await _dedup.TryMarkProcessedAsync(ctx.Message.EventId, ctx.CancellationToken))
return; // already applied -> no-op
await _kpis.ApplyAsync(ctx.Message, ctx.CancellationToken);
}
Exactly-once delivery is a myth; you get exactly-once effect = at-least-once delivery + idempotent consumers.
Block C highlight — resilience (breaker + retry + timeout)
One slow dependency makes its callers block, exhaust their pools, and fail — which fails their callers: a cascade. Each resilience pattern caps one link.
services.AddHttpClient<PaymentClient>()
.AddResilienceHandler("payment", b => b
.AddRetry(new() { MaxRetryAttempts = 3, BackoffType = DelayBackoffType.Exponential, UseJitter = true })
.AddCircuitBreaker(new() { FailureRatio = 0.5, MinimumThroughput = 20, BreakDuration = TimeSpan.FromSeconds(30) })
.AddTimeout(TimeSpan.FromSeconds(3)));
Retry only transient/idempotent calls, with jitter + a budget (fixed-interval retries synchronize into a retry storm that DDoSes the recovering service). Every call needs a timeout (a hung dependency ties up threads forever) and bulkheads isolate pools so one slow dependency can't sink the whole service. A dead-letter queue parks poison messages instead of blocking the queue.
Block D highlight — distributed tracing
A single request fans out across many services; without a trace id propagated through every hop, a failure is un-debuggable — six services' logs and no way to connect them.
// One trace spans every hop; OTel propagates it via the W3C traceparent header.
using var activity = ActivitySource.StartActivity("GenerateReport");
activity?.SetTag("tenant.id", tenantId);
// downstream HTTP + Service Bus calls carry traceparent -> one correlated trace across services.
Pair it with structured + centralized + trace-correlated logs and per-service RED metrics (Rate, Errors, Duration). And deploy schema changes with expand/contract: add the new field (old + new both work) → migrate/backfill → drop the old once nothing uses it — so producers and consumers deploy in any order.
The through-line
Microservices aren't "small services" — they're a bet that independent deployability is worth distributed-systems pain. Everything follows: own your data (database-per-service), talk async where you can, accept eventual consistency and reconcile with sagas + the outbox, make every consumer idempotent, wrap every call in timeouts/breakers/retries-with-jitter, and make the whole thing observable with distributed tracing.
The full guide has all 30 — each with the short answer, the deep dive with code, and the exact senior signal:
Originally published on PrepStack.
Top comments (0)