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.
Analyze the trade-offs between a shared database accessed by many services and a database-per-service pattern. Cover cross-service joins, distributed transactions, reporting/analytics access, data duplication, and eventual consistency on the database-per-service side, and cross-team coupling on schema changes, deployments, and failure isolation on the shared-database side.
Sample Answer
Direct answer
A shared database accessed by many services is operationally simple (one place to back up, one place to query for a report, easy cross-table joins) but structurally couples every service that touches it: a schema change by one team can silently break another team's queries, and there's no way to enforce that only one service writes a given piece of data. Database-per-service removes that coupling, giving each service full control over its own schema and enforcing that it's the only writer of its data, at the cost of needing an explicit strategy for anything that used to be a simple SQL join across tables now owned by different services.
Structured elaboration
With a shared database: cross-service joins are trivial (a single SQL query spanning tables owned by different logical services), transactions across what should be service boundaries are easy (a single atomic, consistent, isolated, durable (ACID) transaction updating two tables), and reporting/analytics can query the whole dataset directly without needing to assemble it from multiple sources. The cost is that these are exactly the things that make it hard to deploy services independently: a schema migration has to consider every service that touches the affected tables, not just the service that conceptually owns them, and there's no enforced boundary preventing one service from reaching into data it doesn't really own.
With database-per-service: each service's schema can evolve on its own release schedule, because no other service can be broken by a change to tables it never had direct access to. The costs show up wherever data used to be joined or transacted across what are now separate services: a query that used to be one SQL join becomes either a replicated read model, an API composition call, or a purpose-built reporting/analytics pipeline that consolidates data from every service (commonly by consuming each service's event stream into a data warehouse); a transaction that used to be a single ACID commit across two tables becomes a saga or another cross-service coordination pattern, accepting eventual consistency where strict atomicity used to be free; and data duplication becomes a deliberate, managed trade-off (a service caching a read-only copy of data it needs from another service) rather than an accident.
Worked example
For reporting and analytics specifically, database-per-service usually means building a separate analytical store (a warehouse or lake) fed by each service's change-data-capture stream or event log, rather than pointing a business-intelligence (BI) tool directly at production service databases; this avoids both the tight coupling of shared production tables and the load a heavy analytical query would otherwise put on a service's transactional database. The operational implications differ sharply by pattern: with a shared database, one team's bad migration or one runaway analytical query can degrade every service at once (a single failure domain); with database-per-service, a failure or slow query in one service's database is isolated to that service, at the cost of needing per-service backup, monitoring, and on-call rather than one shared setup.
You must lead a cross-functional architectural decision while teams disagree about adopting microservices versus staying with a modular monolith. Describe how you would gather objective data, facilitate the technical discussion, build consensus, make a recommendation that balances technical and business goals, and create a measurable plan to validate the decision after the fact.
Sample Answer
Direct answer
When a cross-functional team is split on microservices versus a modular monolith, the way through is to replace the debate with data: define the two or three signals that would actually decide it (current deploy coordination cost, whether any component needs independent scaling, and team-ownership friction), measure them on the real system, and let the measured answer, not the strongest opinion in the room, drive the recommendation.
Structured elaboration
A workable process looks like this: first, separate the technical disagreement from the underlying interests, since "microservices vs. modular monolith" arguments are often proxies for real but unstated concerns (a team wanting more autonomy over its release schedule, or an SRE team worried about operational load from more moving parts); surfacing those interests directly is usually more productive than debating architecture in the abstract. Second, agree on what evidence would settle the disagreement before gathering it, for example current deploy-queue wait times, incident data showing whether failures are concentrated in a few components, and headcount growth projections for the next year, so the data collection isn't retroactively interpreted to fit whichever side is winning the argument. Third, run a small, time-boxed spike, such as extracting one candidate module behind a clean interface first inside the monolith, to surface real integration costs before committing to a full split. Finally, make the recommendation with an explicit, falsifiable success measure attached (for example, "deploy frequency for the extracted service should double within two quarters, or we roll the decision back"), so the decision doesn't become permanent by default just because it shipped.
Worked example
A concrete facilitation sequence: run a short workshop where each side states the specific outcome they're worried about (not the architecture they prefer), collect the deploy-cadence and incident data for the modules under debate, and present both sides with the same evidence before asking for a recommendation, rather than presenting a pre-formed conclusion and asking for buy-in. If the data shows one module already has a measurably different release cadence and on-call profile from the rest, that's the concrete justification for extracting just that module, which often resolves the broader disagreement by making the actual scope much smaller than "microservices vs. modular monolith" implied.
Trade-offs and pitfalls
The most common failure in this kind of facilitation is letting the loudest technical opinion win instead of the data, which produces a decision the losing side doesn't actually buy into and will relitigate at the next disagreement. The second common failure is presenting the recommendation as a permanent, unreviewable architectural commitment rather than attaching a measurable checkpoint; when the plan includes an explicit point to check whether the split delivered what it promised, disagreement about the initial decision matters much less because everyone knows it will be revisited with evidence.
Explain the strangler fig pattern for migrating a monolith to microservices: the role of a routing/intercept layer, the role of anti-corruption layers when the new service must still talk to the old monolith's data, and how you would manage shared-database access during the transition period. Describe how you'd prioritize which components to extract first.
Sample Answer
Direct answer
The strangler fig pattern migrates a monolith to microservices incrementally: you place a routing layer in front of the monolith, redirect one slice of functionality at a time to a newly-built service behind that layer, and let the monolith's role shrink slice by slice until, eventually, it can be retired, rather than attempting a single big-bang rewrite.
Structured elaboration
The routing (or intercept) layer is the mechanism that makes this incremental: it sits in front of the monolith and inspects each incoming request, forwarding requests for the not-yet-migrated functionality to the monolith as before, and requests for the newly-extracted slice to the new service instead. Because the routing decision happens per-request, the cutover for any one slice can be gradual (a percentage of traffic, or a specific subset of users) and reversible (route back to the monolith if the new service misbehaves), rather than an all-at-once switch.
An anti-corruption layer handles the case where the new service still needs to talk to the old monolith, either because the monolith still owns some data the new service needs, or because other parts of the monolith still call into the functionality that's now been extracted. Rather than letting the new service's clean domain model get contaminated by the monolith's legacy data shapes and conventions, the anti-corruption layer translates between the two, so the new service's internal model stays coherent even while it's still dependent on the old system underneath.
Managing shared database access during migration is the trickiest part: in the transition window, both the monolith and the new service may need to read or write data that hasn't fully moved yet. A common approach is dual-write (the monolith continues writing to its tables while also publishing changes the new service consumes) with reconciliation to catch drift, or a change-data-capture stream from the monolith's database that the new service consumes without writing to the monolith's tables directly, avoiding a genuine two-way dependency on the same schema.
Worked example
Prioritizing which components to extract first: start with a slice that's relatively self-contained (few dependencies on the rest of the monolith), has a clear owning team ready to take it on, and delivers a visible win (either operational, like removing a frequent source of incidents, or business, like unblocking a team that's currently release-blocked by the monolith's shared deploy train). Extracting the riskiest, most deeply-entangled part of the monolith first, even if it's the part causing the most pain, tends to produce the highest-risk first migration when the team has the least experience running this pattern; a smaller early win builds the operational muscle (routing, dual-write, monitoring the new service) that the harder extractions will need later.
Trade-offs and pitfalls
The most common failure is leaving the routing layer and the dual-write/reconciliation logic in place indefinitely instead of treating them as temporary migration scaffolding; if a slice never gets fully cut over (the monolith keeps a code path alive "just in case"), the system ends up permanently carrying the complexity of both the old and new implementations, which is worse than either the original monolith or a clean microservice on its own.
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.
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.