Microservices Architecture and Service Decomposition Questions
Decomposing a system into services along bounded contexts, defining service boundaries and ownership, and managing the tradeoffs against a monolith. Covers cohesion, coupling, data ownership per service, the distributed-monolith anti-pattern, and when a modular monolith beats microservices. Emphasizes decomposition reasoning rather than any single framework.
Describe the modular monolith architectural pattern as an intermediate step before adopting microservices. What are its benefits and drawbacks, and what technical and organizational decision criteria would lead you to recommend staying with a modular monolith versus moving to microservices?
Sample Answer
Direct answer
A modular monolith is a single deployable application whose internal code is organized into strictly-separated modules, each owning its own data and exposing a defined interface to the others, the same discipline microservices apply at the network boundary but enforced inside one process instead of across services. It buys most of the maintainability and clear-ownership benefits people associate with microservices (an engineer can reason about one module without understanding the whole codebase) while keeping the deployment, testing, and debugging story of a single application.
Structured elaboration
The main benefits are: one deploy pipeline and one set of infrastructure to operate, no network calls (and their associated latency and failure modes) between modules that used to be function calls, easier cross-module refactors (the compiler or a single test suite catches a broken contract immediately, instead of it surfacing later as a runtime failure between two independently-deployed services), and a natural stepping stone toward microservices if a specific module's traffic or ownership needs later diverge from the rest.
The main drawbacks are: the module boundaries are enforced by discipline and code review, not by a hard network boundary, so they erode more easily under schedule pressure than a boundary that would require actually calling a different service to violate; every module still shares the same deploy (so a bug in one module can still take down the process serving all of them, and a slow test suite in one module still blocks everyone's release); and a module can't scale independently of the others without scaling the whole process.
Worked example
Decision criteria for staying with a modular monolith rather than moving to microservices: the team is small enough that a shared deploy pipeline isn't a bottleneck, no single module has a materially different scaling profile from the rest, and the org values the lower operational overhead (fewer things to monitor, deploy, and keep backward-compatible) more than independent per-module deployability. The Shopify engineering team's widely-discussed decision to run a modular monolith at very large scale is a well-known real-world example: strict internal module boundaries with a shared deployable, extracting specific components to their own services only when a concrete scaling or ownership need justified it.
Trade-offs and pitfalls
The most common failure mode is treating "modular monolith" as a label rather than a discipline: without enforced module boundaries (linting rules, code-ownership checks, or architectural review that catches a module reaching directly into another module's tables), a modular monolith degrades back into an ordinary tangled monolith over time, at which point it has neither the deployability benefits of microservices nor the simplicity benefits of a genuinely modular codebase.
Explain how Domain-Driven Design concepts like bounded contexts, aggregates, and ubiquitous language influence microservice boundaries. Give an example mapping DDD concepts to services for a payments-and-billing domain, and name one mistake architects commonly make when they equate every code module to its own microservice.
Sample Answer
Direct answer
A bounded context is a boundary within which a specific business concept has one precise, consistent meaning; the same word can mean different things in different contexts (an "order" means a customer purchase in the Sales context and a warehouse picking task in the Fulfillment context), and a bounded context is the boundary that keeps those meanings from colliding. It's the natural starting point for drawing service boundaries because a coherent bounded context is already a low-coupling, internally-consistent unit of the domain.
Structured elaboration
A bounded context is defined by its ubiquitous language, the shared vocabulary that a team and its stakeholders use consistently within that boundary; when the same term needs different definitions depending on who's using it, that's usually the signal that you're looking at two contexts, not one. Within a context, an aggregate is the consistency boundary for a single business transaction (for example, an Order aggregate that enforces its own invariants, like "an order's total must match the sum of its line items," as a single atomic unit), and aggregates are the building blocks a bounded context is made of.
For a payments-and-billing domain, a Payments bounded context might own the concepts of a transaction, an authorization, and a settlement, each with its own aggregate and its own precise meaning of terms like "amount" (post-fees or pre-fees, for example); a Billing bounded context might separately own the concept of an invoice and a subscription, where "amount" means something contractual rather than transactional. Even though both contexts touch money, mapping them to two separate services (rather than one "Money" service) follows directly from the fact that their ubiquitous languages and consistency boundaries genuinely differ.
Worked example
The common mistake architects make is equating every code module or every noun in the domain to its own microservice, treating "Bounded Context = Service" as a mechanical, one-to-one rule rather than a starting point for judgment. A bounded context is a good place to START looking for a service boundary because it's already internally coherent, but a very small, tightly-related pair of bounded contexts (say, Authorization and Settlement within Payments, if they share the same team, the same consistency requirements, and change together) can reasonably stay in one service, while a genuinely large bounded context might still need to be split further along a different axis (like read/write access patterns) once it's grown large enough.
Trade-offs and pitfalls
The risk of treating bounded contexts too literally as service boundaries is producing more services than the org can operate well, each one technically "correct" by the Domain-Driven Design (DDD) definition but not justified by any real difference in scaling, ownership, or release cadence. The opposite risk, ignoring bounded contexts entirely and drawing service boundaries along purely technical or organizational lines, tends to produce services whose internal data model is internally inconsistent, because two different meanings of the same term (like "order") end up living in the same service without a clear boundary between them.
For a user-facing operation that needs to compose data from several backend services before it can respond, analyze the trade-offs between calling those services synchronously and handling the composition asynchronously (for example via caching, pre-computation, or an eventual-consistency read model).
Sample Answer
Direct answer
For a user-facing operation composing data from several backend services, calling them all synchronously in parallel and waiting for every response is simplest to build but ties the caller's response time and availability to the slowest and least reliable of the services it calls; handling the composition asynchronously (via caching, pre-computation, or a read model built ahead of time) trades a small amount of freshness for a response time and availability that don't depend on every backend being fast and up at the exact moment of the request.
Structured elaboration
Synchronous fan-out (calling every needed service in parallel and waiting for all responses before replying) is the most straightforward approach and gives the freshest possible data, but it means the composed response is only as fast as the slowest call and only as available as the least reliable service in the fan-out; a single flaky downstream service degrades every request that needs its data, even if the other services involved are healthy. It works reasonably well when the number of services being composed is small, their latency is predictable and low, and there's a clear timeout-and-fallback policy for when one of them is slow (returning a partial result rather than hanging the whole request). The asynchronous alternative pre-computes or caches the composed result ahead of time (for example, a background job periodically assembles the combined view and stores it somewhere fast to read), so the request path only ever reads from one fast, always-available store rather than fanning out live; the cost is that the data shown can be stale by however long the pre-computation lag is.
Worked example
A product page that needs to show price (from a Pricing service), stock availability (from an Inventory service), and personalized recommendations (from a Recommendations service) at once: doing this via a live synchronous fan-out on every page view means a slow Recommendations call (even one that just times out and falls back to "no recommendations shown") shouldn't be allowed to delay or fail the whole page; a well-designed synchronous approach here still uses per-call timeouts and treats each piece as optional with a sensible fallback. An asynchronous alternative would pre-compute the recommendations for a user periodically (recommendations don't need to reflect the last five seconds of behavior) while keeping price and stock as live synchronous calls, since those two genuinely need to be current at the moment of purchase.
Trade-offs and pitfalls
The risk with synchronous fan-out is treating every piece of the composed response as equally critical, so a failure in a genuinely non-essential piece (like recommendations) takes down the whole page instead of just that one section; the fix is deciding, per piece of data, whether it's essential (block on it, with a tight timeout) or optional (fetch it with a shorter timeout and a graceful fallback if it's slow or unavailable). The risk with pre-computed or cached data is using it for something that actually needed to be current (like showing a stale "in stock" status right before checkout), which can produce a worse user experience than a slightly slower live check would have.
Explain how decomposing a system into smaller, well-bounded services can reduce the blast radius of a failure, compared to a single large service that owns many responsibilities. Give an example where splitting a service reduced an outage's scope and made recovery simpler, and describe the trade-off this introduces: more inter-service calls to reason about.
Sample Answer
Direct answer
Decomposing a large service into smaller, well-bounded ones limits how far a single failure can spread, because a bug, resource exhaustion, or outage confined to one small service only takes down the functionality that service owns, instead of a shared process where the same failure could take down every feature that happened to be bundled into it.
Structured elaboration
Blast radius in a monolithic (or overly broad) service is large because everything runs in the same process, shares the same resource pool (memory, connection pool, thread pool), and typically deploys together; a memory leak or a slow downstream dependency in one code path can exhaust shared resources and degrade or crash the whole service, taking unrelated features down with it. Splitting that broad service into smaller, independently-deployed pieces along genuine bounded contexts means each piece has its own process, its own resource pool, and its own deploy and rollback cycle, so a failure in one is naturally contained to the functionality it owns, and recovering it (restarting it, rolling it back) doesn't require touching or redeploying the unrelated pieces.
Worked example
A service that originally handled both order processing and a resource-intensive report-generation feature in the same process: a runaway report-generation job consuming excessive memory could previously degrade order processing too, since they shared the same process and resource pool. Splitting reporting into its own service means a runaway report job now only affects reporting; order processing, running in a completely separate process with its own resources, is unaffected, and the reporting service alone needs to be restarted or scaled to fix the issue, without any customer-facing order-processing impact.
Trade-offs and pitfalls
The trade-off this introduces is more inter-service communication to reason about: what used to be a function call within one process (order processing needing something from the reporting logic, if it ever did) now potentially becomes a network call, which can fail in ways an in-process call can't (timeouts, partial failures, network partitions), and needs its own error handling. Splitting purely for blast-radius reduction without also handling those new failure modes at the boundary (timeouts, retries, fallback behavior) can trade one class of failure (a shared-process crash) for another (an unhandled network failure cascading anyway, just through an HTTP call instead of a function call); the blast-radius benefit only fully materializes when the new inter-service calls are also built defensively.
You are advising a small engineering team (5-8 engineers) with weekly releases, rapidly changing requirements, and unpredictable early traffic on whether to start with a monolith, a modular monolith, or microservices, given a tight 2-3 month deadline to ship an MVP. Recommend an approach and justify it, naming at least five concrete signals (team size, release cadence, traffic pattern, operational maturity, time-to-market pressure) that inform the choice, and explain what would make you revisit the decision as the product and team grow.
Sample Answer
Direct answer
For a 5-8 engineer team shipping weekly with unpredictable traffic and a 2-3 month deadline, I would start with a single deployable application, organized internally into clear modules along the domains you already know (for example users, catalog, orders), rather than splitting into separate services from day one. The reasoning is that microservices add coordination cost (contracts between services, more infrastructure to run, more places a request can fail) that this team doesn't yet have the headcount or release cadence to absorb, while a well-modularized monolith still gets you most of the benefits people actually want from microservices (clear ownership boundaries, the ability to extract a module later) without paying for the ones you don't need yet (independent scaling, independent per-team deploys).
Structured elaboration
Five concrete signals tell you whether to revisit this decision later:
- Team size crossing roughly 15-20 engineers, where a shared deploy pipeline starts to create merge and release contention across teams working on unrelated features.
- One code path needing to scale independently of the rest (for example a spike-prone checkout path while the admin dashboard stays flat), which a monolith can only address by scaling the whole application.
- Release cadence diverging by module, where one team wants to ship several times a day and another wants a slower, more careful cadence, and the shared release train is forcing both onto the same schedule.
- A module's on-call and reliability profile diverging sharply from the rest (frequent incidents in one area shouldn't require redeploying or restarting unrelated code).
- Onboarding friction, where new engineers need to understand the whole codebase to safely change one small part of it, a sign the internal module boundaries have eroded.
Until two or three of these fire together, the operational cost of running and coordinating separate services (service discovery, per-service monitoring, network-call error handling, contract versioning between teams) is usually larger than the coordination pain a monolith is actually causing.
Worked example
Concretely, for the 3-month minimum-viable-product (MVP) window: ship one deployable service with clean internal module boundaries (each module owns its own set of database tables even though they share one database instance, and modules talk to each other through well-defined internal interfaces, not by reaching into each other's tables). This is a modular monolith, not a from-scratch tangle: if traffic later spikes unpredictably in one module, or the team grows and a second team wants to own that module independently, you extract it because the module boundary and its data ownership were already clean. Extracting a well-isolated module is a much smaller project than extracting a module whose code and tables are entangled with everything else.
Trade-offs and pitfalls
The risk of starting monolithic is under-investing in the internal module boundaries because "it's all one deploy anyway," which produces exactly the tangled monolith that makes a later extraction expensive; the fix is to hold the same discipline about module ownership and interfaces you would apply across services, just without paying the network and deployment cost yet. The risk of starting with microservices too early is spending the team's limited early runway on infrastructure (service meshes, per-service CI/CD, contract testing) instead of the product itself, and discovering the wrong service boundaries once real usage patterns emerge, which then requires re-drawing lines across already-deployed services rather than inside a single codebase.
Unlock Full Question Bank
Get access to all 34 Microservices Architecture and Service Decomposition interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.