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.
When would you choose synchronous request/response calls between services versus asynchronous messaging? For each choice, discuss the impact on end-to-end latency, coupling between services, error handling and retry behavior, and the operational implications for on-call and SLOs.
Sample Answer
Direct answer
Choose synchronous calls when the caller genuinely needs the result before it can proceed and can tolerate the callee's latency and availability becoming part of its own; choose asynchronous messaging when the caller can proceed without waiting for the result, or when decoupling the caller's availability from the callee's is more important than getting an immediate answer.
Structured elaboration
Latency: synchronous calls put the callee's latency directly on the critical path of the caller's response time, and a chain of several synchronous calls compounds that (each hop adds its own latency, and the caller waits for the slowest one). Asynchronous messaging removes the callee's latency from the caller's response time entirely, since the caller doesn't wait for the message to be processed. Coupling: synchronous calls create a direct availability dependency (if the callee is down, the caller's request fails or blocks); asynchronous messaging decouples availability, since a message can sit in a queue until the consumer is back up, at the cost of the consumer's effect on the world happening later, not immediately. Error handling: a synchronous call gives the caller an immediate, explicit success-or-failure signal it can act on right away (retry, show an error, fall back); an asynchronous message's failure needs a different mechanism entirely (a dead-letter queue, a retry policy on the consumer side, and some way for the ORIGINAL caller to eventually learn the outcome if it needs to, since it already moved on). Operational implications: synchronous chains make on-call debugging comparatively straightforward (a single request trace shows the whole call chain and where it failed) but make service-level objectives (SLOs, the reliability/latency targets a service commits to) harder to hit as the chain gets longer, since the end-to-end latency and availability are the product of every hop's; asynchronous flows make individual components easier to keep within their own SLOs independently, but debugging "why didn't this eventually happen" requires tracing through queues and consumers rather than a single linear request.
Worked example
A checkout flow illustrates both: charging a customer's card needs a synchronous call to the payment processor, because the checkout page genuinely can't tell the customer "success" until the charge is confirmed, and the caller needs an explicit success-or-failure signal to act on immediately. Sending the order-confirmation email, by contrast, is a good fit for asynchronous messaging: the checkout flow doesn't need to wait for the email to send before showing the customer a success page, and decoupling it means an email-service outage doesn't block checkout at all, only delays the email itself.
Trade-offs and pitfalls
The most common mistake is defaulting to synchronous calls for everything because it's simpler to reason about in the moment, which quietly makes every downstream service's availability and latency a dependency of the caller's SLO, even for work that didn't need an immediate answer. The opposite mistake is making something asynchronous that the caller actually needed an immediate answer for (like the payment charge above), which either forces an awkward polling loop on the caller's side or produces a confusing user experience where the system says "success" before it actually knows whether the operation succeeded.
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.
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.
Design session handling for a web application served by many independently-deployed services. Compare a stateless approach (signed tokens such as JWTs carrying session data) against stateful server-side sessions (a shared session store, or sticky sessions at the load balancer). Discuss the trade-offs for security and revocation, session size, immediate logout/invalidation, and how each approach affects your ability to scale services independently and fail over without dropping user sessions.
Sample Answer
Direct answer
A stateless approach (a signed token such as a JWT (JSON Web Token) carrying the session data) scales cleanly across many independently-deployed services and regions because any service instance can validate the token without needing to look anything up, at the cost of harder revocation and a token that, once issued, is difficult to invalidate before it naturally expires; a stateful approach (a shared session store, or sticky sessions at the load balancer) makes revocation and logout instantaneous, at the cost of needing that shared state to be available, fast, and consistent everywhere a request might land.
Structured elaboration
Security and revocation: a self-contained signed token is valid until it expires, no matter what happens on the server side, so immediate logout or a forced revocation (say, after a password change or a detected compromise) is hard to achieve without an additional mechanism, like a short token lifetime combined with a separate, checked-on-every-request revocation list (which reintroduces some of the shared-state lookup a stateless token was meant to avoid). A server-side session store makes logout and revocation trivial (just delete or invalidate the session record), since every service checks the current, authoritative state on each request. Session size and load balancing: a stateless token carries its own data on every request, so a large session payload adds overhead to every call and needs to stay reasonably small; sticky sessions route a given user consistently to the same backend instance holding their in-memory session, which avoids a shared store but makes load balancing less flexible (that instance failing loses the session, and rebalancing traffic away from a hot instance is constrained by which users are stuck to it) and doesn't scale cleanly across regions, since a user's session is now tied to a specific instance's location.
Worked example
For a web application serving 10M monthly active users across multiple regions, a stateless token is generally the better fit for the base authentication case, since it avoids needing a globally-consistent, low-latency session store that every region's services can reach quickly; the revocation gap is managed by keeping token lifetimes short (minutes to a couple of hours) and using refresh tokens (which ARE checked against a server-side store on each refresh) to balance the low-friction stateless validation for most requests against the ability to revoke access within a bounded, acceptable window. A legacy component that genuinely requires sticky sessions during a migration can be bridged with an adapter: a thin layer that translates the stateless token into whatever session format the legacy component expects, or a temporary session-affinity rule scoped only to that component's traffic, while the rest of the system moves to the fully stateless model.
Trade-offs and pitfalls
The common mistake with stateless tokens is putting too much or too sensitive data directly in the token (since anyone who can decode it, even without the signing key, can read its contents unless it's also encrypted), and underestimating how long a compromised token stays valid if the revocation story isn't designed deliberately. The common mistake with sticky sessions is treating them as a permanent architecture rather than what they usually are in a modern system, a legacy bridge or a stopgap, since they fundamentally limit how flexibly you can load-balance, fail over, and scale across regions compared to a genuinely stateless design.
When should you split a service into two versus keeping it as a single service? Provide measurable indicators (change frequency, team ownership, differing scaling requirements, failure blast radius) and describe a process that avoids premature decomposition while still allowing the service to split later as those signals emerge.
Sample Answer
Direct answer
Split a service into two when at least two of these hold at once: the two parts change at very different frequencies, they're owned (or should be owned) by different teams, they need to scale differently, and a failure in one part shouldn't take down the other. A single one of these signals firing isn't usually enough justification on its own; the process should default to NOT splitting until the signals stack up.
Structured elaboration
Change frequency: if one part of a service is touched every day and another part hasn't changed in a year, bundling them together means every deploy of the fast-changing part carries risk for the stable part too, even though nothing about the stable part actually needed to change. Team ownership: if two different teams are regularly making changes to the same service and stepping on each other's release schedule, that's a coordination cost a split would remove, but only if the teams' work genuinely doesn't need to be coordinated (if they're deeply interdependent, splitting just moves the coordination cost from code review to an API contract). Scaling differences: if one part of a service needs to handle 100x the traffic of another (a hot read path bundled with a rarely-called admin endpoint, for example), scaling the whole service to serve the hot path wastes resources on the cold path. Failure blast radius: if a bug or outage in one part of a service currently takes down an unrelated part, and that unrelated part has a stricter uptime requirement, that's a real argument for isolating them.
The process to avoid premature decomposition: track these signals over time rather than reacting to the first instance of any one of them (one slow deploy or one minor incident isn't a trend), and require at least two signals to agree before committing to a split, since a split that turns out to be unnecessary is expensive to undo (merging two services back into one is rarely done and usually more painful than the original split).
Worked example
A concrete example: an Order service that also handles admin-only bulk data exports. The export feature is used rarely, by a different (internal tooling) team, runs long batch queries that occasionally degrade the Order service's regular request latency, and doesn't need the same uptime service-level agreement (SLA) as live order processing. That's change frequency, team ownership, scaling, and blast radius all pointing the same direction, which is a strong case to split Export into its own service reading from a replica rather than the Order service's primary database.
Trade-offs and pitfalls
The most common overcorrection is splitting on a single weak signal ("these two files feel conceptually different") without checking whether the other three actually agree, producing a service boundary that adds deployment and monitoring overhead without removing any real coordination cost. The opposite mistake, waiting until every signal is screaming before acting, lets a service accumulate enough tangled responsibility that the eventual split becomes a much bigger, riskier project than it would have been if addressed early; the discipline of tracking the four signals over time, rather than reacting to a single incident, is what keeps the timing right in both directions.
Unlock Full Question Bank
Get access to all 36 Microservices Architecture and Service Decomposition interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.