Anyone can draw boxes and arrows and call it a microservices architecture. What separates an architect with a decade or two of production experience from someone reciting a conference talk is the ability to defend where the boundaries sit, what happens when they turn out to be wrong, and how a system of dozens of independently deployable services stays coherent instead of collapsing into a distributed monolith. Interviewers at this level rarely ask "what is a microservice" — they ask you to reason about trade-offs you can only have learned by living through a bad decomposition, a shared database nobody could safely change, or a "simple" library update that quietly required coordinating fifteen deployments. This page works through the questions that come up in senior, lead and architect loops: how to find real service boundaries, who owns which data, when shared code helps versus when it silently re-couples services that were supposed to be independent, and what independent deployability actually demands of your architecture, your contracts and your tests.
Q1 How do you determine correct service boundaries when decomposing a system into microservices?#
Short answer: Draw boundaries around business capabilities and bounded contexts, not around technical layers or database tables. A boundary is correct when the owning team can change the service's internal model and deploy it without coordinating with another team, and wrong when two services can't evolve without moving in lockstep.
Start from the domain, not the schema. Domain-driven design's bounded context is the right unit: a part of the domain with its own consistent model and its own vocabulary, where a term like "Customer" can legitimately mean something different in Billing than it does in Support without that being a bug. A useful heuristic is "what changes together, stays together; what changes independently, gets separated" — if two capabilities are always modified in the same pull request by the same team, they probably belong in one service, and splitting them early just adds network hops with no autonomy payoff. The opposite failure is decomposing by technical layer — an OrderValidationService, an OrderPricingService and an OrderPersistenceService that all have to be called in sequence to process one order. That isn't three services, it's one service's internal layers with a network call stitched between each, and it inherits all the latency and partial-failure risk of distribution with none of the independence benefit.
A boundary is validated in production, not on a whiteboard: a good one can absorb a complete internal rewrite, a new persistence technology or a schema redesign without any other service noticing, because the only thing visible outside the service is its API or event contract. Conway's law is unavoidable here too — service boundaries that don't roughly track team boundaries create constant cross-team coordination regardless of how clean the domain model looks on paper, so a boundary decision is also an organizational decision.
What interviewers look for: reasoning in terms of autonomy and rate of change rather than entity-relationship diagrams, and treating "correct" boundaries as something you validate against how often cross-service coordination is actually needed, not something you get perfectly right up front.
Common mistakes: decomposing by technical layer instead of capability; treating boundaries as a one-time up-front design exercise instead of something you deliberately let emerge — many experienced teams start with a modular monolith and only extract a service once a seam has proven itself under real change pressure.
Follow-up questions:
- How would you split a bounded context that has grown too large to be one service?
- What production signals tell you a boundary is wrong?
Q2 Why should each microservice own its data exclusively, and what actually breaks when two services share a database?#
Short answer: A shared database turns the schema into a second, undocumented API that every service secretly depends on, so any team can break every other service by adding a column, tightening a constraint or changing a type. Each service should own a private schema — or a private database — that nothing outside the service touches directly.
Concretely, sharing a schema removes every benefit independent deployability was supposed to give you. Migrations become cross-team negotiations instead of a single team's decision. One service's query patterns and locks become another service's latency spikes — a noisy-neighbor problem at the data layer that's much harder to diagnose than a noisy-neighbor problem at the network layer. And a shared database tempts developers into wrapping a transaction around writes to "my" tables and "your" tables in the same commit, which quietly re-introduces the tight coupling the split was meant to remove; the moment that transaction exists, the two services can no longer be deployed, scaled or even down for maintenance independently.
Owning data doesn't require owning separate physical hardware — sharing a database server for cost reasons is fine as long as schema and access are isolated, and each service is the only thing that talks to its own tables. When another service genuinely needs a view across two services' data, the answer is not a cross-schema join: it's request-time API composition, a materialized read model built from consumed events (a CQRS-style projection), or, for reporting and analytics, a dedicated pipeline (change data capture into a warehouse) that's explicitly decoupled from the operational path. Interviewers expect you to know what replaces the join, not just that joins are forbidden.
What interviewers look for: treating shared-schema access as a coupling and versioning problem, not a style rule, and having a concrete answer for what replaces cross-service queries once direct access is gone.
Common mistakes: equating "separate schema" with "separate physical server," as though only hardware isolation counts; having no answer for cross-service reporting once direct joins are removed.
Q4 How do you design and evolve API contracts between microservices without breaking consumers?#
Short answer: Treat the API as a versioned, backward-compatible contract owned by the producing team. Additive, non-breaking changes ship freely; breaking changes get a new version alongside the old one; and you catch violations before production with contract tests, not by hoping every consumer reads a changelog.
For synchronous APIs, that means additive changes only within a version — new optional fields, new endpoints, new optional query parameters — while anything that removes a field, changes a type or tightens a required input goes out as a new version that runs alongside the old one for a defined deprecation window, advertised through the contract itself (a deprecation notice, a sunset date) rather than a wiki page nobody reads. Consumer-driven contract testing closes the loop: each consuming team publishes the shape and behavior it actually depends on, and the producer's CI runs those expectations against its real code on every change, which turns a contract break into a failed build in the producer's pipeline instead of a runtime failure in production three deploys later.
The same discipline applies to events, and it's easy to forget because nobody labels an event schema "public API" even though the moment a second service consumes it, that's exactly what it is. Add fields as optional with sensible defaults so old consumers keep working; never repurpose a field's meaning or remove one a consumer might still read; and version the event type explicitly once a genuinely breaking change is unavoidable, publishing both versions during the transition.
// Safe, additive evolution of an integration event contract
public sealed record OrderPlacedEvent(
Guid OrderId,
Guid CustomerId,
decimal Total,
string Currency = "USD"); // new field, optional with a default — old consumers are unaffectedWhat interviewers look for: understanding that a contract test, not a contract document, is what actually prevents outages, and separate strategies for versioning synchronous APIs versus asynchronous event schemas.
Follow-up questions:
- How would you safely retire an old API version once most, but not all, consumers have migrated?
- How do you contract-test an asynchronous event consumer that doesn't expose an HTTP endpoint?
Q5 What should a service template or scaffold standardize across teams, and what should it deliberately leave alone?#
Short answer: A good service template standardizes the boring, high-leverage plumbing every service needs the same way — health checks, structured logging, telemetry, configuration binding, a resilience pipeline, the shape of the CI/CD pipeline — and says nothing about the service's domain model, persistence choices or business logic, because centralizing those re-creates exactly the coupling microservices are meant to avoid.
This is the "paved road" idea: give teams a fast, safe default instead of forcing a shared framework they can't escape. The key property of a template versus a shared library is when it applies — a template shapes a service at creation time, not at every build, so services can diverge afterward without being blocked on a shared dependency's next release. A concrete, current example is the pattern .NET Aspire's project templates follow: an AppHost project for local orchestration, plus a ServiceDefaults project each service includes at creation time to wire up health checks, service discovery and a standard resilience handler — infrastructure plumbing, generated once, not a runtime dependency the whole fleet has to upgrade in lockstep.
public static class ServiceDefaultsExtensions
{
public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
{
builder.Services.AddServiceDiscovery();
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
http.AddServiceDiscovery();
});
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation())
.WithMetrics(m => m.AddAspNetCoreInstrumentation());
return builder;
}
}What the template should not decide: whether a service uses CQRS or simple CRUD, which database it uses, or the shape of its domain model — those belong to the team that owns the bounded context, because centralizing them either produces a framework nobody can safely extend or a team that quietly works around it. Governance here works better as a lightweight "paved road" document plus review than as a mandatory shared codebase.
What interviewers look for: the "paved road, not paved prison" distinction between infrastructure-as-template (low coupling, safe to centralize) and domain-code reuse (high coupling, unsafe), plus awareness of a concrete current example of the pattern.
Q6 How do you test a system made of many independently deployable services?#
Short answer: Push the testing pyramid toward fast, isolated tests inside each service — unit tests for domain logic, integration tests against real dependencies via containers — and replace most cross-service end-to-end coverage with contract tests plus a small set of production smoke tests, because a large end-to-end suite spanning dozens of services is slow, flaky, and becomes the bottleneck that makes independent deployability meaningless in practice.
Inside a single service, the pyramid looks familiar: fast unit tests for business logic with no I/O, then integration tests that exercise the service's own boundary against real infrastructure using containers rather than mocks, because mocking a database hides exactly the serialization and query bugs that show up in production.
public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>, IAsyncLifetime
{
private readonly PostgreSqlContainer _db = new PostgreSqlBuilder().Build();
private readonly WebApplicationFactory<Program> _factory;
public OrdersApiTests(WebApplicationFactory<Program> factory) => _factory = factory;
public Task InitializeAsync() => _db.StartAsync();
public Task DisposeAsync() => _db.DisposeAsync().AsTask();
[Fact]
public async Task PlacingAnOrder_PersistsAndReturnsCreated()
{
var client = _factory.WithWebHostBuilder(b => b.ConfigureServices(services =>
services.AddDbContextPool<OrdersDbContext>(o => o.UseNpgsql(_db.GetConnectionString()))))
.CreateClient();
var response = await client.PostAsJsonAsync("/orders", new { sku = "ABC-1", quantity = 2 });
response.EnsureSuccessStatusCode();
}
}What replaces cross-service end-to-end tests is the contract test layer described earlier: it verifies compatibility without spinning up every dependency, which is what actually lets teams keep deploying independently instead of waiting for a shared, brittle end-to-end suite to go green. What's left after that is deliberately thin — a handful of critical user journeys run against a shared or per-change ephemeral environment, plus canary releases and production synthetic monitoring to catch the remaining risk that pre-production testing can't economically cover. That's a considered trade-off, not a gap to be embarrassed about.
What interviewers look for: a pyramid shaped for a distributed system, with contract tests as the load-bearing addition, rather than "we have a large Selenium suite that hits every service."
Common mistakes: an inverted pyramid where a slow, flaky end-to-end suite is the primary safety net; mocking the database or broker in integration tests, which passes even when real query or serialization behavior is broken.
Q7 What does "independently deployable" actually require at the architecture level, not just the CI/CD level?#
Short answer: It requires that a new version of any one service can go live while every other service keeps running its previous version indefinitely — which means backward-compatible contracts, database migrations that don't assume the new code is live everywhere at once, and no runtime dependency on another service being redeployed at the same time. If deploying service A on a Tuesday and service B three weeks later would cause an incident, you don't have independent deployability, no matter how separate the pipelines look.
The core mechanic is the expand/contract migration: add a new column as nullable and have the new code dual-write to old and new, backfill existing rows, switch reads over to the new column once it's fully populated, and only drop the old column in a later release once you're certain nothing still reads it. A single migration that assumes the new code is already live everywhere is a synchronization point in disguise. Feature flags do the same job for behavior: they decouple deploying code from releasing a feature, so a half-finished capability can sit dormant in production instead of forcing a big-bang cutover.
This is also an organizational property. If shipping a feature always touches four services and needs a coordinated release window, deployability isn't independent even if each service technically has its own pipeline. The test an architect applies is blunt: can team X deploy today without asking team Y? If the honest answer is no, trace exactly which coupling — a shared table, an unversioned contract, a synchronous chain with no fallback — is causing it, and fix that, rather than adding process around the symptom.
What interviewers look for: concrete mechanics (expand/contract migrations, feature flags, contract versioning) instead of treating "independently deployable" as a slogan, and framing it as something measurable today rather than aspirational.
Follow-up questions:
- How do you roll back a service once its expand/contract migration has already run in production?
- How would you coordinate a change that two services genuinely cannot avoid making together?
Q8 "Distributed monolith" is a common label for a failed decomposition. How do you recognize one, and how does it typically happen?#
Short answer: A distributed monolith has all the operational cost of microservices — network calls, serialization, multiple repositories and pipelines, partial failure — with none of the independence benefit, because services still have to deploy together, share a database, or fail as a unit when one of them is down. The tell is simple: if deploying just one service regularly requires changing something else at the same time, that's a distributed monolith regardless of how many repos exist.
The symptoms cluster together: a shared database several services write to directly; synchronous call chains where a request has to pass through service A, then B, then C, then D with no fallback, so any single outage takes the whole chain down; a shared domain library that forces lockstep version bumps; and deployment runbooks that specify an order services must go out in. None of those are exotic mistakes — they're the natural result of extracting services under deadline pressure by lifting an existing layer (an "OrderService" that's really just the old data-access layer with a network hop bolted on) instead of extracting around a real bounded context, or of reaching for a synchronous call and a shared table because it's the fastest way to ship this sprint and the shortcut never gets paid back.
The fix is rarely "split further." Often the healthier move is consolidating a badly cut boundary back into fewer, better-bounded services, or stepping back to a well-modularized monolith until a real reason to extract emerges — more services is not automatically closer to a correct architecture.
What interviewers look for: the specific "can I deploy this one service alone" litmus test, and recognizing that adding more services rarely fixes a distributed monolith — reducing synchronous coupling or consolidating the boundary usually does.
Common mistakes: treating "we run 30 services in Kubernetes" as proof of a healthy architecture regardless of coupling; assuming the remedy for tangled services is always more decomposition.
Q9 How many microservices is "too many," and how do you decide when to split or merge services?#
Short answer: There's no fixed number. The right granularity is the smallest boundary a single team can own, understand and operate end to end without routinely needing another team's help, and "too many" shows up as operational cost — on-call load, cross-service debugging time, infrastructure overhead — exceeding the autonomy you're actually getting back.
Conway's law is the honest starting point: service count should roughly track team structure and cognitive load, not an arbitrary target pulled from a blog post. A small team maintaining fifteen tiny services is usually worse off than the same team owning three well-bounded ones, because the coordination and operational tax — fifteen dashboards, fifteen pipelines, fifteen places a bug could be hiding — swamps whatever isolation benefit the extra splitting bought them.
Watch for signals in both directions. Over-split systems show services that always deploy together anyway (so you paid the network and serialization tax for zero autonomy gain), a single feature that routinely spans five services, or on-call engineers needing a map just to find which service owns a given log line. Under-split systems show a service whose releases are blocked by unrelated teams' unrelated changes, or a test suite that takes far too long because the "one" service is really three domains glued together. Most experienced architects extract a service only when there's a concrete reason — a genuinely different scaling profile, a different team, a different release cadence — rather than pre-emptively splitting along every plausible seam.
What interviewers look for: a cost-versus-benefit framing tied to team autonomy and operational load rather than a target number, and the "extract for a concrete reason" heuristic, which signals judgment over dogma.
Q10 How do you handle cross-cutting concerns — logging, auth, resilience — across dozens of services without duplicating code everywhere?#
Short answer: There are three real levers: push the concern into infrastructure the service doesn't have to code against (a sidecar or service mesh handling things like mTLS and retries at the network layer), standardize it through a project template applied at service-creation time rather than a shared runtime library, or centralize it behind a gateway for traffic entering the system from outside. Which one you reach for depends on whether the concern is genuinely infrastructure-level or needs in-process hooks.
A sidecar or mesh model handles mTLS, retries, timeouts and telemetry uniformly for every service without any of that logic living in application code — upgrading the behavior means upgrading the sidecar, not redeploying every service, at the cost of another operational component and its own learning curve. For concerns that do need to live in-process — structured logging conventions, an authentication handler reading a specific header format, a resilience pipeline tuned to the organization's standard timeout budget — the template model from earlier fits better: bake them into the project template so every new service starts with them, accepting that already-running services only pick up updates when they choose to regenerate that part, which is slower than a mesh but avoids the lockstep-upgrade coupling a shared runtime library creates.
A gateway or backend-for-frontend handles a different axis entirely: north-south traffic entering the system from outside, where centralizing authentication and rate limiting at the edge makes sense precisely because that traffic isn't the east-west, service-to-service concern the mesh and the template are solving.
What interviewers look for: distinguishing infrastructure-layer solutions (mesh or sidecar) from code-layer solutions (template) and matching the concern to the right layer, instead of defaulting to "we'll just make a shared library" for everything.
Follow-up questions:
- When is introducing a service mesh worth it, and when is it overkill for a ten-service system?
- How would you roll out a new cross-cutting requirement, such as a mandatory trace header, across every existing service?
Quick-Fire Round#
| Question | Answer |
|---|---|
| Fastest test for "is this a good service boundary"? | Can the owning team deploy a change without coordinating with another team? |
| Should two microservices ever share a database schema? | No — share a server for cost if you must, never the schema or tables. |
| What's a "shared kernel" in DDD terms? | A deliberate, narrow, explicitly negotiated exception to code isolation between two closely collaborating teams. |
| What migration pattern enables a zero-downtime schema change? | Expand/contract: add and dual-write, backfill, switch reads, then remove the old column later. |
| What replaces a shared library for cross-service compatibility checks? | Consumer-driven contract tests. |
| What's the single best predictor of a distributed monolith? | Services that always have to be deployed together. |
| What does Conway's law predict about service boundaries? | They mirror team communication structure whether you plan it or not. |
| Where should authentication and rate limiting for external traffic live? | At the edge — an API gateway or backend-for-frontend, not in every service. |
How to Prepare#
- Have one real story of extracting a service around a bounded context, and one story about a boundary you got wrong and had to fix.
- Be able to name concretely what breaks when two services share a database, not just say "coupling."
- Rehearse the difference between a shared "paved road" template and a shared runtime library — interviewers use this to separate real experience from cargo-culting.
- Practice explaining the expand/contract migration pattern out loud; it's the mechanic behind "independently deployable."
- Bring a granularity heuristic (team ownership and operational cost) instead of a number.
- Have one concrete cross-cutting-concern rollout story ready: how you shipped a new requirement across many existing services.