Every architect who has shipped both a monolith and a microservices system eventually learns the same lesson: the hard part was never drawing the boxes, it was living with the boundary you drew once real load, real teams and real deadlines hit it. Interviewers use this topic to separate candidates who parrot "microservices for scale" from those who have actually paid the distribution tax and know exactly what it buys and what it costs. At the architect and lead level, expect questions that probe your decision criteria, your read on team structure, and whether you can recognize a system that adopted the microservices label without any of its benefits. This page works through the trade-offs, the migration paths in both directions, and the modular monolith patterns that let a team defer the distribution decision without accumulating a mess they will regret later.

Q1 What factors actually justify choosing microservices over a modular monolith, and which commonly cited reasons don't hold up under scrutiny?#

Short answer: Microservices are justified when you need independent deployability across genuinely separate teams, independent scaling of components with wildly different resource profiles, or strong technology or fault isolation between domains — not simply because a codebase is "getting big" or because a team read that a well-known tech company runs thousands of services.

The reasons that hold up all trace back to organizational or operational independence, not code size. If two teams need to ship on different cadences without coordinating a release train, if one workload needs an order of magnitude more scale than the rest, or if a component genuinely needs a different runtime for good technical reasons, those are real forces pushing toward service boundaries with their own pipelines. The reasons that don't hold up are framed purely in terms of code: "the codebase is too big" is a modularity problem, not a distribution problem, and splitting a tangled monolith into services just distributes the tangle over a network, losing the compiler's help in refactoring across that boundary. "It will make the code cleaner" is backwards — a service boundary should be the consequence of a clean domain boundary already proven out in-process, not a forcing function you hope will create one. The single most reliable predictor of a good outcome is whether the team can already draw module boundaries confidently inside a monolith; if they cannot agree where one bounded context ends and another begins, a network hop between the undefined boundary only makes the ambiguity expensive to fix.

SignalFavors modular monolithFavors microservices
Team structureOne team, or a few teams sharing a release cadenceSeveral teams that need to deploy independently
Scaling profileRoughly uniform load across the domainOne or two components need order-of-magnitude different scale
Domain clarityBoundaries still being discoveredBoundaries proven stable over time
Operational maturityLimited platform/SRE investmentMature CI/CD, observability and on-call practices already in place
Failure isolation needLow — a shared failure mode is acceptableHigh — one component's failure must not take down the rest

What interviewers look for: whether you frame the decision around team and operational forces rather than code aesthetics, and whether you can name the specific, falsifiable conditions that would flip your recommendation instead of giving a generic "it depends."

Common mistakes: treating microservices as a default "mature" architecture that every growing system eventually needs, and citing a large tech company's service count as evidence without accounting for the scale of engineering investment that made it work there.

Q2 Explain Conway's Law and how you would use team topology to choose or design around an architecture.#

Short answer: Conway's Law observes that a system's design mirrors the communication structure of the organization that builds it, so if you design a service boundary that cuts across a single team's daily work, or leave one team owning a "boundary" that spans two organizational units, the software will drift back toward matching the org chart regardless of the diagram you drew.

The practical consequence is that architecture and team design are the same decision viewed from two angles; treating them separately is how you end up with services that are technically separate but require two teams to coordinate every change — the worst of both worlds, since you paid the distribution tax without buying deployment independence. The Team Topologies model gives useful vocabulary here: a stream-aligned team owns a cohesive slice of the domain end to end, platform teams provide the paved road (CI/CD, observability, shared infrastructure) that stream-aligned teams consume as a service, and enabling teams temporarily lend expertise without taking ownership. When choosing service boundaries, "which team will own this in production, and does its cognitive load allow it" should carry as much weight as any technical analysis, because a boundary no single team can hold in their head will accrue debt at the seams regardless of how cleanly it was drawn. This also explains why the "reverse Conway maneuver" works: reorganizing teams around the target architecture before the code fully reflects it tends to pull the code toward that shape, because day-to-day ownership does more to enforce a boundary than a diagram ever will.

What interviewers look for: that you can connect an abstract law to a concrete decision — naming which team owns which boundary and why — rather than reciting Conway's Law as a quote without application.

Follow-up questions:

  • How would you redesign team boundaries before attempting to split a monolith along the same lines?
  • What happens to a service boundary when the owning team is reorganized or split?

Q3 How do you structure a modular monolith in .NET so module boundaries are actually enforced, not just documented?#

Short answer: Enforce boundaries the same way you enforce any contract in .NET — with the compiler and the project system, not a wiki page — by giving each module its own project, exposing only a narrow public surface through interfaces, marking everything else internal, and forbidding modules from referencing each other's internals or database tables directly.

A workable layout puts each business capability in its own class library project (Orders, Catalog, Billing), with a single small *.Contracts namespace per module holding the interfaces, DTOs and integration events other modules may depend on; everything else is internal, so a compile error — not a review comment — stops another module reaching into a repository it should not touch. Cross-module calls go through those public interfaces via dependency injection, and cross-module data access never happens through a shared DbContext reaching across schemas; each module owns its own tables and other modules either call its API in-process or subscribe to the events it publishes, exactly as they would over a network. This is what makes a modular monolith a stepping stone rather than a trap: if boundaries already behave like service boundaries for data and communication, extracting one later means swapping an in-process call for an HTTP call, not a redesign.

C#
// Orders.Contracts/IOrderService.cs — the only surface Billing is allowed to depend on
public interface IOrderService
{
    Task<OrderSummary> GetOrderAsync(Guid orderId, CancellationToken cancellationToken);
}

// Orders/OrderService.cs — internal implementation, invisible outside the module
internal sealed class OrderService(OrdersDbContext db) : IOrderService
{
    public async Task<OrderSummary> GetOrderAsync(Guid orderId, CancellationToken cancellationToken)
    {
        var order = await db.Orders.FindAsync([orderId], cancellationToken)
            ?? throw new KeyNotFoundException($"Order {orderId} not found.");
        return new OrderSummary(order.Id, order.Total, order.Status);
    }
}

A lightweight architecture test in the build (using a library like NetArchTest or a hand-rolled reflection check) that fails when a module references another module's internal namespace turns this from a convention into something CI actually enforces.

What interviewers look for: concrete enforcement mechanisms — project boundaries, internal visibility, per-module data ownership, and automated checks — rather than "we just agree not to cross boundaries," which is the answer that predicts the monolith will erode within two quarters.

Common mistakes: putting every module in the same project with folders instead of project references (nothing stops an illegal reference at compile time), and letting modules share one DbContext against the same tables, which quietly recreates a shared-database coupling that is harder to unwind than the original monolith.

Q4 What is a "distributed monolith," and what specific symptoms in production reveal that you have built one?#

Short answer: A distributed monolith is a system that has been split into separately deployed services but still has to be deployed together, tested together and understood as a single unit to change safely — meaning it has all the coordination cost of a monolith plus all the latency, partial-failure and operational cost of a distributed system, with none of the benefits of either.

The clearest symptom is the release calendar: if shipping one service routinely requires coordinating a compatible release of two or three others, you have not achieved independent deployability, you have just moved the coupling from a shared solution file to a shared release train, and it is now harder to see because no tool enforces it. A second symptom is synchronous call chains that fan out three or more services deep to satisfy one user request — every hop adds latency and a new failure mode, trading an in-process call that fails only if the process is down for a distributed one that fails far more often. A third symptom is a shared database, or worse, services reading each other's tables directly — the "services" have no real data ownership and any schema change becomes a cross-team negotiation, exactly the coupling microservices are meant to remove. A fourth, subtler symptom is a test suite where the only way to get confidence is standing up every service together, because contracts between them are not independently verifiable — a strong sign the boundaries were drawn along the wrong seams.

What interviewers look for: recognition that "distributed monolith" is a coupling diagnosis, not a service-count diagnosis, and the ability to name concrete, observable symptoms (release coordination, deep synchronous chains, shared databases) rather than a vague "it's badly designed."

Common mistakes: assuming that having many small services automatically avoids this problem — service size has nothing to do with it; coupling does.

Q5 What does moving a call from an in-process function invocation to a network call actually cost, beyond the obvious latency?#

Short answer: It converts a call that can only fail if the process itself is down into one that can fail for a long list of new reasons — network partition, timeout, the callee being mid-deploy, a version-incompatible contract, back-pressure from an overloaded dependency — and every one of those failure modes has to be designed for explicitly with retries, timeouts, circuit breakers and idempotency, none of which an in-process call ever needed.

Latency is the cost people expect; the ones that actually cause incidents are consistency and testability. An in-process call participates in the same transaction as everything around it for free — DbContext.SaveChangesAsync either commits everything or nothing. A network call cannot: the caller has already committed its own state, or is about to, and now needs a strategy — a saga, an outbox, eventual consistency with compensating actions — for its own write succeeding while the downstream call never lands, or lands twice. That shows up in production as a bug class ("charged but never shipped") that simply cannot happen with an in-process call. Testability suffers similarly: a unit test for in-process logic runs in milliseconds with no infrastructure; verifying behavior across a network boundary needs contract tests, a running dependency or a convincing fake, plus fault-injection tests for "what happens when this call times out" that in-process code never has to answer.

C#
// The in-process version has one failure mode: an exception from ProcessPaymentAsync.
var receipt = await paymentService.ProcessPaymentAsync(order, cancellationToken);

// The distributed version needs an explicit resilience policy for the new failure modes
// a network hop introduces — this is not boilerplate, it is new behavior to design and test.
var receipt = await resiliencePipeline.ExecuteAsync(
    async ct => await paymentClient.ChargeAsync(order.ToChargeRequest(), ct),
    cancellationToken);

What interviewers look for: that you name consistency and testability, not just latency — latency is the cost everyone already expects, so leading with the transactional and testing implications signals real production experience.

Q6 How do you handle data ownership when decomposing a system that currently shares one database?#

Short answer: Give each service or module exclusive write ownership of its own tables, have every other consumer go through that owner's API or its published events rather than querying its tables directly, and use patterns like the transactional outbox to keep "update my data" and "notify everyone else" consistent without a distributed transaction.

The migration usually happens in stages. First, even before touching physical ownership, assign a single logical owner to each table and stop any other module writing to it — this alone surfaces most of the illegal coupling, because code that used to reach across the shared schema now has to go through an API, and that friction makes the coupling visible in review. Next, replace cross-boundary reads with either a synchronous call to the owner's API for data that must be current, or a locally cached, eventually consistent read model built from its published events for data read far more often than it changes — an order-fulfillment service does not need a live join against the catalog on every request. For the write side, the transactional outbox pattern is the standard answer: the service writes its state change and an "event to publish" row in the same local transaction, and a separate relay process reads the outbox and publishes to the broker, guaranteeing the event is never lost or published without the state change having actually committed.

C#
// Both writes happen in one local transaction, so the event can never be
// published for a state change that didn't actually commit, or vice versa.
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken);

order.MarkShipped();
db.OutboxMessages.Add(new OutboxMessage(
    Type: nameof(OrderShipped),
    Payload: JsonSerializer.Serialize(new OrderShipped(order.Id, order.ShippedAtUtc))));

await db.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);

What interviewers look for: the outbox pattern named specifically, and an understanding that "shared database" is fixed by assigning ownership and changing access patterns, not simply by giving each service its own connection string to the same tables.

Common mistakes: splitting the physical database without first fixing logical ownership, which just means two services now maintain separate, silently diverging copies of data neither of them clearly owns.

Q7 Design a concrete, incremental path for extracting one capability out of a monolith into its own deployable service.#

Short answer: Treat the module you already isolated as a strangler-fig candidate: put a routing seam in front of the capability, stand up the new service behind that seam while it still calls into the old in-process implementation, cut traffic over once the new service proves itself against real load, and only then delete the old code path — never attempt a single big-bang cutover for a component with real production traffic.

Concretely: first finish the in-process modular monolith work from the earlier question, so the capability already has a narrow public interface and owns its own tables — extraction without that step means extracting a tangle, not a module. Second, introduce a routing layer, often a reverse proxy such as YARP or an API gateway, in front of the endpoints you intend to move, so individual routes can redirect to the new service without touching every caller. Third, stand up the new service and either share the existing database temporarily (an explicitly temporary compromise) or run a dual-write or change-data-capture pipeline that replicates the relevant tables so it reads from its own store from day one. Fourth, cut traffic over incrementally — by route, tenant or percentage — comparing responses against the old path with a shadow or canary strategy before trusting it fully. Finally, once the new path has run cleanly for a defined stabilization period, remove the old implementation and the seam's fallback; leaving it in "just in case" indefinitely is how strangler migrations stall out half-finished for years.

JSON
{
  "Routes": {
    "orders-new": {
      "ClusterId": "orders-service",
      "Match": { "Path": "/api/orders/{**catch-all}" }
    },
    "orders-legacy": {
      "ClusterId": "monolith",
      "Match": { "Path": "/{**catch-all}" }
    }
  }
}

What interviewers look for: a sequenced, reversible plan with an explicit cutover mechanism and a defined point where the old path is actually deleted — vague answers ("we'd extract it into a microservice") without a routing and rollback strategy suggest the candidate has not done this against a live system.

Q8 When and why would a team deliberately consolidate microservices back into a monolith or a smaller set of services?#

Short answer: When the operational cost of running many independently deployed services — the on-call burden, the cross-service debugging, the infrastructure spend — is measurably larger than the coordination cost the split was supposed to remove, and especially when the team that owns those services never actually grew to match the number of boundaries.

This is not a rare or shameful outcome; it is a normal correction when a system was decomposed ahead of the organizational or traffic growth that would have justified it. The signal is usually financial and operational before it is architectural: infrastructure and observability costs scale with service count almost independently of load, so ten lightly used services can cost more to run than one moderately used one, and a small team maintaining ten pipelines, dashboards and runbooks spends a large fraction of its capacity on the tax of distribution rather than on features. The fix is rarely "merge everything back" wholesale; it is usually consolidating a cluster of chatty, always-co-deployed services that never achieved independent release cadence, while keeping genuinely independent, high-value boundaries as they are. Recognizing this early is itself a sign of maturity, and interviewers listen for whether a candidate treats consolidation as a failure to hide or as a legitimate, evidence-driven decision like any other.

What interviewers look for: willingness to describe reversing a decision without treating it as an admission of incompetence, plus a cost-based rather than purely aesthetic justification for when to merge services back.

Q9 How do bounded contexts from domain-driven design guide where you draw module or service boundaries, and what happens when you get them wrong?#

Short answer: A bounded context is the boundary within which a domain model and its vocabulary are internally consistent — "Customer" means one specific thing inside it — and drawing module or service boundaries along bounded contexts, rather than along technical layers or database tables, is what keeps each unit independently understandable; getting it wrong produces a boundary that looks clean on a diagram but requires constant translation and coordination in practice.

The practical exercise is event storming with the people who actually run the business process, listening for where the same noun starts meaning something different or a conversation breaks into "that's not really our problem, that's theirs." A Customer in the sales context — a lead with contact details and a sales stage — is not the same entity as a Customer in billing — an account with a payment method and an invoice history — even though both modules might casually call it "the customer table" if the boundary is drawn wrong. When two contexts do need to talk, the pattern is an anti-corruption layer: a small translation adapter that converts the other context's model into your own vocabulary, so a schema change on their side does not silently leak in and corrupt yours. Getting the boundary wrong most often looks like a module that cannot complete an operation without synchronously calling two or three "neighboring" contexts every time — evidence the boundary split a single concept in half, and the fix is almost always to redraw it, not add more integration code.

C#
// Anti-corruption layer: Billing translates Sales' model into its own vocabulary
// instead of depending directly on Sales' shape.
internal sealed class BillingCustomerTranslator
{
    public BillingAccount ToBillingAccount(SalesCustomerDto salesCustomer) => new(
        AccountId: salesCustomer.CustomerId,
        LegalName: salesCustomer.CompanyName,
        BillingEmail: salesCustomer.PrimaryContactEmail);
}

What interviewers look for: familiarity with the anti-corruption layer as a named, deliberate pattern, and the instinct to redraw a boundary rather than add integration glue when a boundary keeps requiring synchronous cross-context calls.

Follow-up questions:

  • How would you detect that a bounded context boundary is wrong from telemetry alone, without a design review?
  • What is the difference between a bounded context and a database schema?

Q10 A team wants microservices "for scalability," but only two of twelve planned endpoints actually need to scale independently. How do you respond, and what would you recommend instead?#

Short answer: Ask what specifically needs independent scaling and why, because "scalability" is almost never a property of the whole system — it is a property of one or two hot paths — and recommend a modular monolith with those one or two capabilities carved out as separately scaled services, keeping everything else together until a similar, evidence-backed need appears.

The first move is turning "scalability" from a slogan into a number: what request volume, at what latency target, is the hot path expected to hit, and does the rest of the system come anywhere close? Most systems show a sharp, Pareto-shaped distribution — one search endpoint or ingestion pipeline dominates load while the other ten are administrative CRUD that could run on a laptop. Splitting all twelve into services to solve a problem only two of them have means paying the full distribution tax — new pipelines, new failure surfaces, cross-service calls for what used to be one transaction — for eight endpoints that gained nothing. The recommendation matching the evidence is to keep the modular monolith, ensure the two hot-path modules already have clean boundaries and their own data ownership, and extract only those two using the earlier extraction playbook; horizontal scaling of the monolith itself is also worth stating explicitly, since it solves uniform load growth without splitting anything, and is usually the cheaper first lever. This demonstrates the core skill interviewers check for at this level: turning a vague requirement into a falsifiable one before committing to an architecture that is expensive to reverse.

What interviewers look for: the instinct to demand numbers before agreeing with the premise, and a proportionate recommendation — extract what actually needs it, don't split everything because part of it needs it.

Quick-Fire Round#

QuestionAnswer
What single factor most reliably predicts a good microservices outcome?Genuine organizational need for independent deployability, not code size.
What does Conway's Law predict about a service boundary that cuts across one team?It will drift back toward matching the team's actual communication structure.
What is the clearest production symptom of a distributed monolith?Releases of separate services routinely have to be coordinated together.
What C# access modifier is the primary enforcement tool for a module boundary?internal, combined with per-module project references.
What pattern keeps a local state change and an event publish consistent without a distributed transaction?The transactional outbox pattern.
What is the first infrastructure piece introduced when strangling a capability out of a monolith?A routing layer (reverse proxy or API gateway) in front of the target routes.
What DDD pattern protects a bounded context from another context's model leaking in?An anti-corruption layer.
Why might a team consolidate microservices back into fewer services?The operational cost of running them exceeds the coordination cost they removed.
What should you ask before agreeing a system needs microservices "for scale"?Which specific endpoints need independent scaling, and by how much.
What is usually the cheaper first lever before splitting a service for scale?Horizontally scaling the existing monolith or module.

How to Prepare#

  • Be ready to argue both sides: a case for starting with a modular monolith, and a case for extracting one service early, each backed by specific conditions rather than general preference.
  • Practice naming the concrete symptoms of a distributed monolith from telemetry and release history, not just the definition.
  • Have one clear explanation of the transactional outbox pattern and why "save, then publish" is unsafe without it.
  • Rehearse the extraction playbook end to end — boundary hardening, routing seam, data migration, canary cutover, deletion of the old path — since interviewers often ask for it as a single walkthrough.
  • Prepare a generic, numbers-free story about recommending against microservices, or about consolidating services back, since it signals judgment over dogma.
  • Review how Conway's Law and Team Topologies vocabulary (stream-aligned, platform, enabling teams) connects to boundary decisions, since architects are expected to reason about teams, not just code.