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.
Explain the architectural principles loose coupling, high cohesion, separation of concerns, and single responsibility as they apply to drawing service boundaries. For each principle, give a concrete example of a boundary decision it would push you toward, and explain how violating it shows up later (harder deployments, blurred ownership, cascading changes).
Sample Answer
Direct answer
Loose coupling means two components can change independently without breaking each other; high cohesion means everything inside one component is closely related and serves one clear purpose. Separation of concerns and single responsibility are the design habits that get you there: separation of concerns keeps distinct kinds of logic (say, business rules versus data storage versus presentation) from tangling together, and single responsibility says each unit (a service, a class, a module) should have one reason to change.
Structured elaboration
At the service-boundary level, loose coupling shows up as: Service A can deploy a new version without Service B needing to change or even redeploy, as long as A's external contract (its API) stays the same. High cohesion shows up as: everything Service A does relates to one clear responsibility (an Order service handles order lifecycle, not also user authentication and email formatting). Violating loose coupling typically looks like two services sharing a database table directly, so a schema change in one silently breaks the other; violating high cohesion typically looks like a "God service" that has accumulated unrelated responsibilities over time because it was the easiest place to add one more feature.
Concretely: if Service A calls Service B synchronously and depends on B's internal implementation details (say, the exact error codes B's database driver happens to return) rather than B's documented API contract, that's tight coupling, because a change B makes to its internals, even one that doesn't change its documented behavior, can break A. The fix is depending only on the contract, not the implementation, and versioning that contract explicitly when it does need to change.
Worked example
Single responsibility applied to service boundaries: an Order service that also sends marketing emails when an order ships has taken on a second responsibility unrelated to order-lifecycle management; a change to the email template now requires redeploying and retesting the Order service, and a bug in the email-sending code can take down order processing even though the two are logically unrelated. Splitting email notifications into their own service (triggered by an OrderShipped event rather than embedded in the Order service's code) restores single responsibility: each service has one reason to change, and a failure in one doesn't take down the other.
Trade-offs and pitfalls
Violating these principles rarely happens all at once; it's usually death by a thousand small compromises, each individually reasonable ("it's faster to just add this one field to the existing service instead of creating a new one") that accumulate into a service nobody wants to touch because it has too many unrelated responsibilities and too many hidden dependents. The counter-pressure is real too: splitting too aggressively in the name of single responsibility produces services so narrow that every business operation requires coordinating five of them, trading one kind of complexity (a tangled service) for another (an over-fragmented one); the goal is a boundary that's cohesive enough to reason about on its own and loosely coupled enough to change independently, not maximal fragmentation.
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.
Compare the responsibilities of an API Gateway to a Service Mesh. Explain what each solves: north-south traffic (client-to-service) for the gateway versus east-west traffic (service-to-service) for the mesh, where their responsibilities overlap, and describe an architecture that legitimately uses both together, noting the added operational cost of running sidecars at scale.
Sample Answer
Direct answer
An API gateway handles north-south traffic (requests coming in from clients outside the system) and a service mesh handles east-west traffic (calls between services inside the system); they solve related but distinct problems, and a mature architecture at scale typically uses both together rather than choosing one over the other.
Structured elaboration
The API gateway sits at the system's edge and is the single entry point for external clients: it handles authentication of external callers, rate limiting per client, routing to the right backend, and TLS termination for public traffic. The service mesh sits inside the system, between services that never talk to an external client directly, and handles mutual TLS between those internal services, retries and traffic shifting for internal calls, and detailed observability of internal service-to-service traffic. Where they overlap: both can do routing, both can enforce some form of authentication, and both contribute to observability, which is a common source of confusion, but the gateway's routing decisions are about which service should handle an external request, while the mesh's routing decisions are about how one internal service reaches another (including things like canary-shifting a percentage of internal traffic to a new version of a downstream service).
Worked example
An architecture using both: an external client calls the API gateway, which authenticates the request, applies a per-client rate limit, and routes it to the appropriate front-line service; that front-line service then needs to call three other internal services to assemble its response, and each of those internal calls goes through the mesh, which handles mutual TLS between them, applies a consistent retry policy, and produces detailed tracing for that internal call chain. The client only ever sees the gateway; the mesh is entirely invisible to anyone outside the system.
Trade-offs and pitfalls
The performance and operational cost of running both together is real: every internal hop through the mesh adds its own sidecar latency, and running both a gateway cluster and a mesh's sidecars everywhere means two separate pieces of infrastructure to operate, monitor, and upgrade. The common mistake is either using the gateway alone to also handle internal service-to-service traffic (which forces east-west calls through an edge-oriented component not designed for that volume or pattern, and misses the mesh's mTLS-by-default benefit for internal calls) or introducing a mesh before there's a real need for its internal-traffic benefits, paying its operational cost for a problem that a smaller service count doesn't yet have.
Describe a pragmatic process to decompose a large, high-traffic monolith using Domain-Driven Design: the steps from domain discovery, through forming bounded contexts, to extracting the first services and the domain events that decouple them. Name the pitfalls to avoid (shared-database anti-patterns, premature splitting, boundaries with no clear owning team) and how you would validate a candidate boundary (spike tests, consumer usage data) before committing to extraction.
Sample Answer
Direct answer
A pragmatic Domain-Driven Design (DDD) driven decomposition process for a large, high-traffic monolith runs in four stages: domain discovery with the people who actually understand the business, drawing candidate bounded contexts from that discovery, validating those candidates against real usage data before committing to extraction, and extracting the first service behind a boundary that also decouples it with domain events rather than direct calls.
Structured elaboration
Domain discovery typically uses a facilitated exercise like event storming, where domain experts and engineers map out the business events that happen in the system (OrderPlaced, PaymentAuthorized, InventoryReserved) without worrying about current code structure; clusters of related events and the language used to describe them are the raw material for candidate bounded contexts. From there, candidate contexts get validated against real signals before any code moves: does this candidate context have a genuinely different team owner, a genuinely different release cadence, or genuinely different scaling needs from its neighbors? A lightweight way to validate is a lower-cost spike, like standing up the candidate service's interface behind a feature flag while the implementation still lives in the monolith, or examining real consumer-usage data (which callers actually depend on this piece of functionality, and how tightly) before extraction.
Once a candidate is validated, extraction proceeds by first identifying the domain events the new context needs to publish or consume (OrderPlaced triggering a downstream Inventory reservation, for example) so that the new service is decoupled from its neighbors through events rather than direct synchronous calls into the old monolith's internals. Pitfalls to avoid at this stage: a shared-database anti-pattern, where the new service and the old monolith both read and write the same tables during a transition period (this defeats the purpose of the extraction and makes rollback harder, not easier); premature splitting, extracting a context before its boundary and ownership are validated, which tends to produce a service that has to be pulled back or merged with another later; and lack of ownership, extracting a service without a clear team committed to operating it, which leaves it as an orphaned piece of infrastructure nobody prioritizes maintaining.
Worked example
For a monolith that needs to split along bounded contexts under real traffic, a practical sequencing is: run event storming with the Payments and Fulfillment domain experts, notice that "Order" means two different things in each conversation, treat that as the boundary signal, validate by checking how many call sites in the current monolith actually cross that boundary versus stay within it (a high cross-boundary call count is a red flag that the proposed split doesn't match how the code is actually used today), and only then extract Fulfillment as its own service, publishing an OrderPlaced event that Fulfillment subscribes to instead of the monolith calling into Fulfillment's code directly.
Trade-offs and pitfalls
The single most common failure in this process is skipping the validation step and going straight from a whiteboard exercise to extraction, because the whiteboard boundary that looked clean in a discovery workshop often doesn't match how the current code and its call graph are actually structured; validating against real usage data before extracting is what prevents a costly "extract, discover it's wrong, and merge it back" cycle.
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.
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.