Microservices-specific interview questions go beyond general system design into the patterns — sagas, service boundaries, sidecars — unique to distributed service architectures.
A microservice is a small, independently deployable service built around a single business capability, owning its own data and its own release lifecycle. The size isn't really the point — a service can be a few hundred lines or several thousand — what matters is that it can be developed, tested, deployed, and scaled without coordinating a release with other teams. Just carving a monolith into ten codebases that still share a database, deploy together, and call each other's internal methods directly gives you a distributed monolith, which is arguably worse than the original because you've added network latency and partial failure without gaining any of the independence. The real signal of a good microservice boundary is that a team can change its internal implementation, schema, or even language and framework without breaking any consumer, as long as the public contract stays stable. So the definition is organizational as much as technical: it's a unit of independent change.
The most reliable technique is domain-driven design's concept of bounded contexts, where you identify the natural seams in the business domain — areas where the same word means different things or where a group of entities and rules change together — and make each bounded context its own service. For example, "Customer" in a billing context has very different attributes and invariants than "Customer" in a support-ticketing context, and forcing them into one shared model creates constant coupling and merge conflicts between teams. A good heuristic is to look at Conway's Law and match service boundaries to team boundaries, since a service owned by a single team that can deploy it independently tends to stay healthy, while one split across teams becomes a coordination bottleneck. You also want high cohesion inside a service and loose coupling between services, meaning data and behavior that change together should live together, and cross-service calls should be rare enough that a chatty back-and-forth isn't required for common operations. Getting boundaries wrong is expensive to fix later, so many teams deliberately start with a modular monolith and extract services once the real seams become obvious from actual usage patterns.
In a monolith a multi-step business transaction can just wrap everything in a single ACID database transaction, but once the steps span multiple services with their own databases, you can't use a two-phase commit in practice because it doesn't scale and creates tight coupling and locking across services. The saga pattern solves this by breaking the transaction into a sequence of local transactions, each committed independently in its own service, with each step publishing an event or message that triggers the next step. If a step fails partway through, the saga runs compensating transactions for every step that already succeeded, undoing their effects in reverse order rather than rolling back a single atomic transaction. For example, an order saga might reserve inventory, charge a payment, and schedule shipping, and if the payment charge fails, a compensating action releases the inventory reservation. The tradeoff is that the system is only eventually consistent during the saga's execution, and you as the developer have to explicitly design and test the compensating logic, which is real business logic, not just a generic rollback.
In choreography, there's no central coordinator; each service publishes events when it finishes its local transaction, and other services subscribe to the events relevant to them and react by performing their own local transaction and publishing further events. This keeps services decoupled and avoids a single point of failure, but as the number of steps grows it becomes hard to see the overall flow because the business process is implicitly scattered across every service's event handlers. Orchestration introduces a dedicated orchestrator that explicitly calls each service in sequence, tracks the state of the saga, and triggers compensating transactions if a step fails. This makes the flow much easier to understand, test, and monitor because the logic lives in one place, but the orchestrator itself becomes a critical component and can turn into a god object if it starts encoding too much business logic that should live in the individual services. Most teams start with choreography for simple two-or-three-step flows and move to orchestration, often using a tool like Temporal or Camunda, once a saga involves more than a handful of steps or needs complex retry and timeout logic.
The strangler fig pattern is a strategy for migrating a legacy monolith to microservices incrementally rather than through a risky big-bang rewrite, named after the vine that grows around a host tree and gradually replaces it. You put a routing layer, often an API gateway or reverse proxy, in front of the monolith, and start extracting one capability at a time into a new service; the router sends traffic for that capability to the new service while everything else still goes to the monolith. Over time you keep peeling off functionality into new services, and the monolith shrinks until it's "strangled" down to nothing or to a small core that's not worth extracting further. The key benefit is that the system keeps running and delivering value throughout the migration, each extracted piece can be validated in production before you move to the next, and you can stop or reverse course at any point without having thrown away a rewrite. The main challenge is handling data that's still shared between the old monolith and the newly extracted service during the transition, which often requires synchronization or a temporary shared-read pattern until the data ownership fully moves over.
Synchronous communication, typically REST over HTTP or gRPC, is simple to reason about because the caller sends a request and blocks until it gets a response, which maps naturally to request-response use cases like a client asking for a specific resource. Its downside is temporal coupling: if the downstream service is slow or down, the caller is blocked or fails too, and a chain of synchronous calls across several services multiplies latency and creates cascading failure risk. Asynchronous communication, using a message broker or event stream like Kafka or RabbitMQ, decouples the caller from the callee in time; the producer publishes an event or message and moves on, and the consumer processes it whenever it's ready, which improves resilience and lets services scale independently. The tradeoff is added complexity: you now have eventual consistency instead of an immediate answer, you need to handle message ordering, duplicate delivery, and dead-letter queues, and debugging a flow that hops across several async handlers is harder than following a synchronous call stack. In practice, most systems use gRPC or REST for low-latency queries where you need an immediate answer, and events or messaging for anything that triggers a side effect or a multi-step workflow like the sagas we discussed.
In the database-per-service pattern, each microservice owns its own database or schema, and no other service is allowed to read from or write to it directly; all access goes through the owning service's API. This preserves the independence that's the whole point of microservices, because a team can change its schema, switch database technology, or add an index without checking whether some other service's queries depend on the old structure. Sharing a database across services breaks that isolation: it creates a hidden coupling where a schema change in one service silently breaks another, it makes independent deployment nearly impossible because you have to coordinate migrations, and it lets services bypass each other's business rules and validation logic by writing directly to tables. The obvious cost of database-per-service is that you lose the ability to do a simple SQL join or a single ACID transaction across data owned by different services, which is exactly why patterns like sagas and CQRS-style read models exist. A common compromise for reporting or complex cross-service queries is to maintain a denormalized read replica or a data warehouse fed by events, rather than querying each service's operational database directly.
Because services deploy independently, you can't assume every consumer has upgraded to match a producer's latest API, so you need a strategy that lets old and new clients coexist. The most common approach is to design changes to be backward compatible whenever possible, meaning you add new optional fields rather than removing or renaming existing ones, and you treat consumers as needing to ignore fields they don't recognize. When a breaking change is unavoidable, teams typically version the API explicitly, either in the URL path like /v2/orders, in a custom header, or via media-type versioning in the content-type, and run both versions side by side until every consumer has migrated off the old one. For event-driven systems the same idea applies to event schemas, and tools like a schema registry with Avro or Protobuf enforce compatibility rules, such as only allowing backward-compatible changes to be published without a version bump. Consumer-driven contract testing is what actually gives you confidence that a schema or API change won't break a specific downstream consumer before you deploy it.
An API gateway is a single entry point that sits between external clients and the internal mesh of services, so a client doesn't need to know the network location or internal structure of dozens of services. It typically handles cross-cutting concerns centrally, like TLS termination, authentication and authorization, rate limiting, request routing to the right backend service, and sometimes response aggregation when a single client request needs data from multiple services. This is valuable because it keeps that logic out of every individual service, but it also means the gateway can become a single point of failure or a performance bottleneck if it's not built to scale horizontally and fail gracefully. A common pitfall is letting the gateway accumulate business logic over time, since that recreates the tight coupling and slow release cycle of a monolith, just moved into the gateway layer. Some architectures use a backend-for-frontend variant, where different gateways are tailored to different client types like mobile versus web, instead of one generic gateway trying to serve everyone.
In a monolith, a stack trace or a single log file usually shows you the entire call path for a request, but in microservices a single user action might fan out across a dozen services, each with its own logs, so there's no single place to see the whole picture. Distributed tracing solves this by propagating a trace ID and span IDs through every request as it hops between services, typically via context propagation headers following something like the W3C Trace Context standard, and tools like Jaeger, Zipkin, or the OpenTelemetry ecosystem stitch those spans back together into one end-to-end trace. This lets you see exactly where time was spent, which service introduced latency, and where an error originated, which is nearly impossible to reconstruct from grepping separate log files after the fact. Beyond tracing, full observability in a microservices system also requires correlated structured logging, so every log line carries the same trace ID, and metrics with consistent labeling across services, because any one of the three pillars alone gives you an incomplete picture. The practical challenge is instrumentation discipline: every service, including third-party or legacy ones you don't fully control, needs to propagate the trace context correctly, or you get gaps in the trace that make debugging just as hard as before.
The sidecar pattern deploys a helper process alongside each service instance, typically in the same pod in Kubernetes, that intercepts and handles cross-cutting concerns like TLS, retries, timeouts, load balancing, and metrics collection, without the service's own code needing to implement any of that. A service mesh like Istio is essentially this pattern applied systematically across an entire fleet of services: it installs a sidecar proxy, usually Envoy, next to every service instance, and those proxies collectively form a data plane that handles all service-to-service traffic, while a control plane pushes configuration like routing rules, mTLS certificates, and traffic policies to every proxy. The big benefit is that application developers stop writing retry logic, circuit breakers, and observability instrumentation in every language and every service, since it's handled uniformly at the infrastructure layer instead. The cost is added operational complexity and latency, since every call now hops through two proxies, one on each side, and debugging becomes trickier because there's another layer between your code and the network. Teams usually adopt a service mesh once they have enough services that inconsistent per-service networking code has become a real reliability and security problem, not from day one.
In a monolith, an integration test can spin up the whole application in one process, but in microservices you can't realistically spin up every dependent service, along with its database and its dependencies, just to test one service in isolation, and doing so in CI would be slow and flaky. The classic failure mode is that each team tests their service against a mocked version of the services they depend on, everything passes in isolation, but the real deployed services break because someone changed a field name or a status code and nobody caught it before production. Contract testing, using a framework like Pact, addresses this by having the consumer define an explicit contract of exactly what it expects from the provider's API, in terms of request shape and response shape, and that contract is then run against the actual provider service in the provider's own CI pipeline. This catches breaking changes at build time on the provider's side, without needing a full end-to-end environment, because the provider knows the exact expectations of every consumer that has published a contract against it. Teams typically layer this with a small number of true end-to-end tests for the most critical user journeys, since contract tests handle the bulk of cross-service compatibility risk far more cheaply than full integration environments.
Partial failure is a defining characteristic of distributed systems: any given call to another service can fail, hang, or time out independently of the caller, and if you don't design for that explicitly, one slow or failing service can cascade and take down services that depend on it, even if those services themselves are healthy. The circuit breaker pattern addresses the cascading part by tracking failures to a downstream dependency and, once failures cross a threshold, "opening" the circuit to fail fast on further calls for a cooldown period instead of letting requests pile up waiting on a dependency that's clearly unhealthy. The bulkhead pattern addresses a related but different problem, resource exhaustion, by isolating the resources, like thread pools or connection pools, used to call each downstream dependency, so that if one dependency becomes slow, it only exhausts its own dedicated pool rather than starving the resources needed to call every other dependency. Named after the watertight compartments in a ship's hull, the idea is that one compartment flooding shouldn't sink the whole ship. In practice these patterns are combined with sensible timeouts and retries with backoff, and libraries like resilience4j or a service mesh's built-in traffic policies implement most of this without every team hand-rolling it.
The overhead of microservices — network latency, eventual consistency, distributed tracing, more infrastructure to run — is real, and the pattern is only worth it if you get something significant in return, which is usually deployment independence: the ability for one team to build, test, and ship changes to their service on their own schedule, without coordinating a release with every other team in the organization. In a monolith, even a one-line change requires the entire application to be rebuilt, retested, and redeployed together, so the blast radius of any single deploy is the whole system, and release cadence tends to slow down as the codebase and the number of contributing teams grow. With independently deployable services, a team can deploy multiple times a day if they want, roll back just their own service if something breaks, and scale only the specific service that's under load rather than scaling the entire application. This independence is also what enables using a different technology per service, letting a team pick the language or database best suited to their specific problem, though most organizations deliberately limit this to avoid an unmanageable sprawl of tech stacks. The catch is that this benefit only materializes if service boundaries are drawn well and teams actually own their services end to end, because if every feature still requires touching three services and coordinating three deploys, you've paid for the distributed systems complexity without getting the organizational speed you were after.
The problem it solves is keeping a database write and a message publish consistent with each other: if a service updates its own database and then separately publishes an event to a message broker, a crash between those two steps means either the write happened with no event ever sent, or the reverse, and there's no distributed transaction spanning a database and a broker to make both atomic. The outbox pattern fixes this by writing the event into an 'outbox' table in the same local database transaction as the actual business data change, so both either commit together or roll back together, guaranteed by the database's own ACID transaction. A separate background process, or a change-data-capture tool like Debezium reading the database's write-ahead log, then picks up new outbox rows and publishes them to the message broker, marking them as sent once delivery succeeds. This guarantees at-least-once delivery of the event exactly when, and only when, the corresponding data change actually committed, which is essential for sagas and other event-driven flows that depend on never silently missing an event.