Practice microservices architecture interview questions grouped by experience level, from service design to distributed systems patterns.
Junior (0-2 years)
An approach to building an application as a collection of small, independently deployable services, each responsible for one specific piece of business functionality, communicating with each other over a network, typically through APIs. Each service can be developed, deployed, and scaled independently of the others.
A monolith bundles an entire application's functionality into a single codebase and a single deployable unit. Microservices split that same functionality across multiple independent services, each with its own codebase, its own deployment pipeline, and often its own database, communicating with each other over the network rather than through direct in-process function calls.
Independent deployment lets one service ship a change without redeploying the entire application. Independent scaling lets you allocate more resources specifically to the services that actually need them, rather than scaling an entire monolith uniformly. Teams can also work more independently, each owning a specific service without needing to coordinate on every single change across the whole codebase.
Distributed systems are inherently more complex than a single application, network calls can fail in ways an in-process function call never would, and testing a full end-to-end flow across many services is genuinely harder. Operational overhead also grows substantially, since more services means more things to deploy, monitor, and keep running reliably.
A bounded context defines a clear boundary around a specific piece of business functionality, within which a particular model and its terminology have one single, consistent meaning. It matters because microservices are typically organized around these boundaries, so each service owns a clearly defined slice of the business domain, rather than being split along arbitrary or purely technical lines.
It means a specific service can be updated, redeployed, and released to production without needing to redeploy any other service alongside it. This is one of the core, defining benefits of the architecture, since it lets teams ship changes to their own service on their own schedule, without waiting on or coordinating a large, synchronized release across every other service.
Most commonly through HTTP-based REST APIs, where one service makes an HTTP request to another and waits for a response. Some architectures also use message queues or event brokers for asynchronous communication, where a service publishes a message without waiting for an immediate response from whatever consumes it.
Synchronous communication means the calling service sends a request and waits for a response before continuing, like a typical REST API call. Asynchronous communication means the calling service sends a message and continues on immediately, without waiting for the receiving service to actually process it right away.
REST (Representational State Transfer) is an architectural style for building APIs around resources, identified by URLs, and standard HTTP methods (GET, POST, PUT, DELETE) acting on them. It's commonly used because it's simple, widely understood, and works over plain HTTP, which nearly every platform and language already supports natively.
An API contract defines exactly what a service's API expects as input and what it returns as output, its request and response formats. It matters because multiple teams and services depend on that contract staying stable and predictable, and a service that changes its contract unexpectedly can break every other service that calls it.
A message queue lets one service place a message onto a queue for another service to process later, decoupling the sender from needing the receiver to be immediately available or fast. It solves the problem of tight coupling between services, letting the sender continue its own work without waiting on the receiver, and letting the receiver process messages at its own pace even under a sudden burst of load.
Sending a welcome email after a user signs up is a good fit for asynchronous communication, since the signup itself shouldn't be delayed or fail just because the email service is temporarily slow or briefly unavailable. Checking whether a specific item is in stock before confirming an order, on the other hand, typically needs a synchronous call, since the calling service genuinely needs that answer before it can proceed.
An idempotent operation produces the same result no matter how many times it's repeated, which matters enormously for retries, since a network timeout might mean the original request actually succeeded but the response was simply lost in transit, and a safe retry needs to avoid accidentally duplicating that same action, like charging a payment twice. A common way to achieve this is having the caller include a unique idempotency key with the request, so the receiving service can recognize and safely ignore an exact duplicate.
Applied to a microservice, it means each service should own one specific, cohesive piece of business capability, like order management or user authentication, rather than a mix of unrelated responsibilities. The underlying idea is the same as applying it to a class, one clear reason to change, just applied at the much larger scale of an entire, independently deployed service.
Organize services around business capabilities and bounded contexts, like a Customer service, an Order service, and a Payment service, rather than splitting purely along technical layers, like a database-access service and a business-logic service. Splitting by business capability tends to keep each service more cohesive and reduces the amount of chatty, cross-service communication needed to complete a single, typical business operation.
Loose coupling means a change to one service's internal implementation doesn't require changing another service, as long as the actual API contract between them stays the same. It's important because tightly coupled services largely defeat the whole point of splitting into microservices in the first place, since a change to one would still ripple out and force changes to others anyway.
The public API is the specific contract other services actually depend on, its endpoints, request and response formats. The internal implementation is everything else, the actual code, the database schema, the specific business logic, which can be changed freely as long as the public API's external behavior stays consistent. This distinction is exactly what lets a team refactor or rewrite a service's internals without breaking any other service that depends on it.
Sharing a database creates hidden, unmanaged coupling between the two services, since either one can inadvertently break the other by changing a shared table's schema, defeating the intended independence a microservices architecture is meant to provide. Each service typically owns its own data instead, and other services access that data only through the owning service's actual API, not by querying its underlying database directly.
A versioning strategy defines how a service introduces a breaking change to its API without immediately breaking every existing consumer still depending on the old version, commonly through URL-based versioning like /v1/ and /v2/. A microservice needs one specifically because multiple other services or clients likely depend on it, and they can't all realistically be updated to a new contract at the exact same instant.
It means each microservice owns and exclusively manages its own database, with no other service ever accessing that database directly. It's common because it enforces the loose coupling microservices are meant to have, and lets each service independently choose the specific database technology that genuinely best fits its own particular data needs.
Data duplication means the same piece of information, like a customer's name, might be stored in more than one service's own database, each keeping its own local, relevant copy. In a monolith, this would typically be considered poor practice, an unnecessary violation of normalization. In microservices, it's often an intentional, acceptable trade-off that avoids services needing to constantly call each other just to look up commonly needed data.
Eventual consistency means that after an update, different services might briefly show slightly different, temporarily out-of-sync versions of related data, but they'll eventually converge to a consistent state once all the relevant updates have fully propagated. It commonly shows up in microservices because data is spread across multiple independent databases, and keeping all of them instantly and perfectly synchronized in real time isn't practically feasible without largely reintroducing the tight coupling the architecture is trying to avoid.
Strong consistency guarantees that any read immediately after a write reflects that write, with no possible delay or staleness. Eventual consistency allows a brief window where a read might return a stale, outdated value before everything fully catches up and converges. Eventual consistency is far more common in microservices, since strong consistency across independent, separately-owned databases is genuinely difficult and expensive to achieve reliably at scale.
The owning service publishes an event whenever its data changes, like OrderCreated or CustomerUpdated, and any other service that needs to keep a local copy subscribes to that event and updates its own local data accordingly whenever a relevant event arrives.
A distributed transaction needs to update data across multiple separate services (and their separate databases) as a single, atomic, all-or-nothing unit. It's genuinely difficult in microservices because there's no single shared database transaction spanning all of them, and coordinating a reliable, all-or-nothing outcome across multiple independent, network-connected services introduces real complexity and potential failure points that a single-database transaction never has to deal with.
A container packages an application together with everything it needs to run, its dependencies, its runtime, its configuration, into one single, portable unit that runs consistently across different environments. It's commonly used for microservices because it lets each service be deployed and run independently and predictably, with no risk of one service's dependencies conflicting with another's on the same underlying machine.
Docker is the most widely used tool for building, packaging, and running containers. It solves the it works on my machine problem, ensuring an application runs the exact same way in development, in testing, and in production, since the container bundles the exact same runtime environment everywhere it's actually deployed.
A virtual machine virtualizes an entire computer, including its own full operating system kernel, which makes it heavier and slower to start. A container instead shares the host machine's underlying operating system kernel and only packages the application and its specific dependencies, making containers significantly lighter and much faster to start than a full virtual machine.
It's what actually lets different teams ship changes to their own services at their own pace, without needing to coordinate a large, synchronized release across every other service in the system. Without it, splitting an application into separate services provides far less real, practical benefit, since you'd still be stuck deploying everything together in lockstep, much like a monolith.
A deployment pipeline automates the steps needed to build, test, and deploy a service, code commit, automated tests, build, deploy. Each microservice typically has its own pipeline specifically so it can be deployed on its own independent schedule, without waiting on or being blocked by any other service's own separate pipeline.
A container registry, like Docker Hub or a private equivalent, stores built container images so they can be pulled and run on any server or cluster that needs them. It's the actual distribution point between building a service's container image and actually running that same image in a target environment, whether that's a testing environment or production.
An API gateway sits in front of all the individual backend microservices, acting as a single, unified entry point for external clients. It routes an incoming request to the correct backend service and can also centrally handle cross-cutting concerns like authentication, rate limiting, and logging, so individual services don't each need to reimplement that same logic separately.
Without a gateway, every client needs to know about and directly manage connections to every individual backend service, which becomes genuinely unmanageable as the number of services grows. A gateway also lets you change or add backend services without clients needing to know or care, since they only ever talk to the one stable, unified gateway address.
Service discovery lets one service find the actual current network location of another service it needs to call, without that address being hardcoded anywhere. It's needed because service instances are frequently created, destroyed, and moved around, especially in a containerized, auto-scaling environment, so hardcoding a fixed IP address or hostname simply wouldn't stay accurate or reliable for very long.
Load balancing distributes incoming requests across multiple running instances of the same service, so no single instance gets overwhelmed while others sit comparatively idle. It matters because running multiple instances of a service is exactly how you scale it horizontally to handle more overall traffic, and load balancing is what actually makes that horizontal scaling effective in practice.
An in-process function call within a monolith is fast, reliable, and essentially can't fail due to a network issue. A call from one microservice to another travels over an actual network, which introduces real latency, and can genuinely fail due to a network partition, a timeout, or the other service simply being temporarily unavailable, failure modes a monolith's internal function calls never need to worry about at all.
It's one of several classic, well-known assumptions developers new to distributed systems tend to make incorrectly, that a network call will always succeed and return quickly and predictably, the same way a local, in-process function call would. It's relevant to microservices specifically because every single service-to-service call depends on the network, so a design that doesn't explicitly account for network failures, delays, or partial outages will eventually run directly into real, painful problems in production, often at the worst possible time.
Mid-Level (3-6 years)
REST is simple, human-readable, and works well for external-facing or loosely-coupled APIs where broad compatibility matters most. gRPC uses a more efficient binary protocol and strongly-typed contracts, fitting internal, performance-sensitive service-to-service communication where both ends are under your own control. Message-based communication, through a broker, fits scenarios needing genuine decoupling and resilience to a receiver being temporarily unavailable, at the cost of giving up an immediate, synchronous response.
In event-driven architecture, a service publishes an event describing something that already happened, like OrderPlaced, without knowing or caring which other services, if any, are actually listening for it. Request-response instead requires the calling service to know exactly which specific service to call and to wait directly for its response, creating a more direct, immediate dependency between the two services involved.
In pub-sub, a publisher sends a message to a topic without knowing which specific services, called subscribers, are actually listening, and any number of subscribers can independently receive and react to that same message. It solves the problem of tight coupling between a producer and its consumers, letting new subscribers be added later without requiring any change at all to the original publishing service.
In point-to-point messaging, a message placed on a queue is delivered to and consumed by exactly one consumer, even if several consumers are actively listening on that same queue. In pub-sub, a message published to a topic is delivered to every single subscriber currently listening to that topic, not just one of them.
Service A can maintain its own local, cached copy of the specific data it needs, kept up to date by subscribing to relevant events published by Service B whenever that data actually changes. This trades a small amount of eventual consistency, the local copy might be very briefly stale, for a substantial gain in speed, since Service A no longer needs a slow, synchronous network call on every single request.
A Saga coordinates a sequence of local transactions spread across multiple services to achieve an overall, larger business operation, since a single traditional distributed transaction spanning multiple databases genuinely isn't practical in a microservices architecture. If a step partway through fails, the Saga triggers compensating actions to undo the effects of the steps that already completed successfully, rather than the whole operation ever having true, database-level atomicity across all the services involved.
In choreography, each service listens for events from other services and decides independently what to do next, with no single, central coordinator managing the overall flow. In orchestration, a central orchestrator service explicitly tells each participating service what to do and in what order, coordinating the entire Saga's flow directly and visibly from one central place.
It's an action that semantically undoes the effect of a previous step in a Saga, once a later step in that same Saga fails. If an Order service successfully reserved inventory but a subsequent Payment step then failed, a compensating transaction would explicitly release that previously reserved inventory back, rather than leaving it stuck in an inconsistent, permanently reserved state.
CQRS separates the model and code path used for writing data (commands) from the model and code path used for reading data (queries), sometimes even backed by entirely separate, independently optimized databases. It's useful when read and write workloads have genuinely different requirements, like needing to serve a very high volume of reads efficiently from data that's written comparatively rarely.
Event sourcing stores every single change to an entity as an immutable sequence of events, rather than storing and overwriting just its current, latest state directly. The entity's current state is then derived by replaying all of its recorded events in order. This gives you a genuinely complete, auditable history of every change, at the cost of real additional complexity compared to simply reading and overwriting a row's current state.
A circuit breaker monitors calls to a downstream service, and after a certain threshold of failures, it stops sending further calls entirely for a defined period, failing fast instead of continuing to wait on a service that's already clearly struggling or down. It solves the problem of a struggling downstream service being repeatedly hammered with more requests, and prevents that struggling service's failure from cascading and consuming resources in the calling service too.
Closed, the normal state, where requests flow through as usual. Open, triggered once failures cross a defined threshold, where requests fail immediately without even attempting the actual call. Half-open, entered after a defined cooldown period, where a limited number of test requests are allowed through to check whether the downstream service has genuinely recovered before fully closing the circuit again.
A retry pattern automatically re-attempts a failed call, which works well for handling brief, transient failures like a momentary network blip. Implemented naively, with no backoff or limit, a burst of retries can actually make an already-struggling downstream service's situation meaningfully worse, adding even more load onto a service that's already failing under its current load.
Exponential backoff increases the delay between successive retry attempts, doubling or otherwise increasing it each time, rather than retrying immediately and repeatedly at a fixed, constant interval. It's paired with retries because it gives a struggling downstream service genuine breathing room to actually recover, rather than being hit again and again in rapid succession right as it's already struggling.
The bulkhead pattern isolates resources, like a thread pool or a connection pool, for calls to a specific dependency, so that dependency failing or slowing down doesn't exhaust resources needed for calls to a completely different, unrelated dependency. The name comes from a ship's bulkheads, physical compartments that contain flooding to one specific section, preventing the entire ship from sinking due to damage in just one localized area.
A timeout defines the maximum time a caller will wait for a response before giving up and treating the call as failed. Without an explicit timeout, a call to a hung or extremely slow downstream service can block the caller indefinitely, tying up its own resources and potentially causing that hang to spread to whatever else depends on the caller in turn.
A service registry is a database of currently available service instances and their actual network locations. Service discovery is the broader process of a service actually looking up and finding another service's current location, and it typically does that lookup by querying the service registry directly.
In client-side discovery, the calling service itself queries the service registry directly and picks which specific instance to call. In server-side discovery, the calling service simply sends its request to a fixed, well-known location, like a load balancer, which then handles querying the registry and routing the request to an actual instance on the caller's behalf.
Client-side load balancing has the calling service itself decide which specific instance to send a request to, distributing that decision logic across every single caller. Server-side load balancing centralizes that decision in a dedicated load balancer component that every request passes through, which is generally simpler to manage centrally but does introduce an additional network hop for every single request.
A load balancer or service registry periodically checks whether each service instance is actually healthy and able to serve requests correctly, commonly through a dedicated health check endpoint each instance exposes. This is needed because sending traffic to an instance that's actually unhealthy or has already crashed would simply result in failed requests, defeating the entire purpose load balancing is meant to serve.
Orchestration automates deploying, scaling, and managing a large number of containers across a cluster of machines, handling things like restarting a failed container automatically, or scaling the number of running instances up or down based on actual current load. It solves the real problem of manually managing potentially hundreds of containers across many machines, which quickly becomes genuinely impractical to do reliably by hand at any real scale.
Kubernetes is the most widely used container orchestration platform, managing the deployment, scaling, networking, and self-healing of containerized applications across a cluster of machines. In a microservices architecture, it's commonly used to run and manage each service's containers, handling much of the operational complexity that comes with running many independent, distributed services reliably.
A Pod is the smallest deployable unit in Kubernetes, typically wrapping one container (though it can technically wrap more than one closely related container that need to share resources). Kubernetes schedules, scales, and manages Pods directly, rather than managing individual containers on their own, in isolation.
It automatically adjusts the number of running Pod instances for a service based on actual observed metrics, most commonly CPU or memory usage. It's useful because traffic to a service naturally fluctuates over time, and automatically scaling instance count up during genuine periods of high demand, then back down during quieter periods, keeps the service both responsive under load and cost-efficient when demand is genuinely low.
Senior (6-8 years)
Distributed tracing tracks a single request's complete path as it flows through multiple services, recording timing and outcome information at each individual step along the way. It becomes genuinely necessary because a single user-facing request in a microservices architecture might touch a dozen different services, and without tracing, figuring out exactly which specific service actually caused a slowdown or a failure becomes largely guesswork.
A correlation ID is a unique identifier generated at the very start of a request and passed along, typically in a header, to every single downstream service call that request triggers. Every service logs that same correlation ID alongside its own log entries, letting you later reconstruct and follow a single request's complete, end-to-end journey across every service it actually touched by simply searching logs for that one shared ID.
Logging captures discrete, individual events with contextual detail, useful for understanding exactly what happened at one specific point in time. Metrics capture aggregated, numerical data over time, like request rate or error rate, useful for spotting broader trends. Tracing captures a single request's complete path across multiple services, useful for understanding exactly where time was actually spent and where a specific failure genuinely occurred within a larger, multi-service flow.
Chaos engineering involves deliberately injecting controlled failures, like killing a service instance or introducing artificial network latency, into a running system to genuinely verify it actually handles those failures gracefully, rather than just assuming it does based on the design alone. Teams practice it because a distributed system's real resilience under actual failure is often quite different from what the architecture diagram alone would suggest, and finding that gap in a controlled, deliberate test beats discovering it for the first time during a genuine, unplanned production incident.
Correlate alerts using tracing data or a dependency graph, so a single root-cause failure, like a shared database going down, is surfaced clearly as one primary alert, with the resulting downstream service failures explicitly grouped under it rather than firing as dozens of separate, seemingly unrelated alerts. Without that correlation, an on-call engineer can genuinely waste significant time chasing what looks like many separate, unrelated problems that are actually all just symptoms of the exact same single underlying root cause.
An SLI is an actual, measured metric, like request latency or error rate. An SLO is an internal target for that metric, like 99.9% of requests completing successfully. An SLA is a formal, often contractual commitment to a customer regarding that same target, typically carrying real, defined consequences if it's genuinely not met. SLOs are usually set somewhat stricter than any external SLA, deliberately leaving a safety margin before an actual contractual commitment would genuinely be at real risk of being broken.
The write side handles commands and updates a write-optimized data store, typically normalized for consistency and data integrity. The read side maintains a separate, often denormalized data store specifically optimized for fast, efficient querying, kept in sync with the write side through events. The trade-off is the read side's data can lag briefly behind the write side, introducing a form of eventual consistency, in exchange for meaningfully better read performance at genuine scale.
Rebuilding an entity's current state by replaying every single event from the very beginning becomes progressively slower as the total number of events for that entity keeps growing over time. A snapshot periodically saves the entity's fully computed current state at a specific point, so rebuilding it later only requires replaying events that occurred after that most recent snapshot, rather than replaying its entire, complete history every single time from scratch.
Version the event schema explicitly, and write code that can correctly interpret and process both the old and new event versions during the actual replay process, rather than assuming every stored event will always match the current, latest schema version. This is genuinely more involved than a typical database schema migration, since old events are permanent, immutable historical facts that must remain correctly interpretable indefinitely, not something that gets updated or rewritten in place.
Data mesh extends microservices' idea of each service genuinely owning its own data to the analytical and reporting side of a system too, treating data specifically as a product that each domain team is responsible for producing, documenting, and maintaining, rather than centralizing all analytical data into one single, shared data team's data warehouse. It's a natural extension of decentralized ownership applied specifically to analytics rather than just operational, transactional data.
Data directly involved in financial transactions, like an account balance, typically needs strong consistency, since even a brief, temporary inconsistency there could genuinely cause real, tangible harm. Data like a product's view count or a user's activity feed can usually tolerate eventual consistency just fine, since a brief delay there has no real, meaningful practical consequence for anyone.
Lead (8-10 years)
A service mesh, like Istio or Linkerd, handles service-to-service communication concerns, like load balancing, retries, encryption, and observability, at the infrastructure layer itself, typically through a lightweight proxy sidecar deployed alongside each individual service instance. It solves the problem of every single service needing to independently implement the same networking and resilience logic itself, centralizing those genuinely cross-cutting concerns instead into shared infrastructure that every service automatically benefits from.
A sidecar deploys a helper container alongside a main application container, handling a specific supporting concern, without the main application needing to implement or even be aware of that concern itself. A service mesh uses this pattern by deploying a proxy sidecar next to every single service instance, intercepting all of that service's network traffic to transparently handle retries, encryption, and metrics collection, all without the actual application code needing any changes to support it.
A BFF is a dedicated backend service tailored specifically to one particular frontend client's exact needs, like a separate BFF for a mobile app versus one for a web application, rather than forcing every different type of client to consume the exact same generic, one-size-fits-all API. It's useful when different clients have genuinely different data or performance needs, like a mobile app needing a smaller, more compact payload than a desktop web application would.
The gateway routes requests to either the monolith or the appropriate new microservice based on the specific request path, using what's often called the strangler fig pattern, gradually moving individual functionality out of the monolith and into new services behind that same stable gateway, without clients ever needing to know or care which backend is actually currently handling their specific request.
Named after a plant that gradually grows around and eventually replaces its host tree entirely, the pattern incrementally routes specific pieces of functionality away from an old monolith and into new microservices, one small piece at a time, until the monolith is eventually fully replaced, rather than attempting one single, large, risky rewrite all at once. It's common because it lets a team keep genuinely shipping value throughout a long migration, rather than pausing all other feature work for an extended, high-risk rewrite effort.
Feature flags, combined with consistent request-level routing (like routing based on a hashed user ID) at the API gateway or service mesh layer, let you route a specific subset of real traffic to a new version of a service while the majority of traffic continues to the existing, stable version. Coordinating this correctly across multiple interacting services genuinely requires the flag's specific state to be consistently and reliably propagated wherever it's actually needed within that particular request's full lifecycle.
Support multiple API versions concurrently for a defined, planned deprecation period, and use API gateway-level routing or transformation logic to adapt a request from an older client version into whatever format the current backend services actually expect. Forcing a hard, unplanned cutover risks genuinely breaking older client versions that a meaningful number of real users may still actively be running, especially on mobile, where users often can't be forced to update immediately.
Issue a signed token, typically a JWT, at login, containing the user's identity and relevant claims. Each downstream service then independently validates that token's signature and claims locally, without needing a network call back to a central authentication service on every single request, which would otherwise add real, unnecessary latency and create a single, central point of failure for every request across the entire system.
User authentication verifies the identity of an actual human end user. Service-to-service authentication verifies that a request genuinely originated from a legitimate, trusted internal service rather than an unauthorized or malicious source, commonly implemented using mutual TLS or a separate, distinct service-issued token, entirely independent of any specific end user's own identity or credentials.
In mTLS, both sides of a connection, not just the client as in standard, typical TLS, present and verify a certificate, confirming both parties' identities to each other genuinely mutually. It's commonly used internally within microservices specifically to ensure that only legitimately authorized, trusted services can actually communicate with each other, which a service mesh commonly implements and enforces automatically across the whole fleet of services.
Automated linting or contract validation checks, run in each service's own CI pipeline, can consistently and automatically enforce agreed-upon governance standards without needing a slow, manual review process for every single API change. A shared, well-documented API design guide, backed by real, working automated tooling rather than relying purely on manual enforcement or after-the-fact review, tends to actually get consistently followed in practice at real scale.
A centralized, dedicated secrets manager, like HashiCorp Vault or a cloud provider's own equivalent, stores secrets securely and each individual service retrieves what it specifically needs at startup or on demand, rather than secrets being hardcoded into configuration files or environment variables checked directly into source control. This also meaningfully enables proper secret rotation without requiring every single service to be manually updated and redeployed each time a specific secret is actually rotated.
Centralized authorization policies, often enforced consistently at the API gateway or the service mesh layer, combined with structured, consistent audit logging across every single service, give visibility into exactly who accessed what specific data and precisely when. Leaving access control entirely up to each individual service's own separate, independent implementation makes a genuinely consistent audit trail across the whole system substantially harder to reliably achieve and maintain.
Staff (10+ years)
I'd weigh it against real, concrete organizational needs, genuine team scaling issues where multiple teams are actively stepping on each other in the same codebase, or specific components that genuinely need to scale or deploy independently, rather than adopting microservices simply because it's currently the more fashionable or widely-discussed architectural approach. A well-structured, well-modularized monolith is often the genuinely better fit for a smaller team or an earlier-stage product, and adopting microservices prematurely mostly just adds real operational overhead without a matching, actually-needed benefit.
The strangler fig pattern, incrementally extracting specific, well-bounded pieces of functionality one at a time behind a stable gateway, lets a team keep shipping real feature work throughout a long migration, rather than pausing everything else for a large, high-risk, all-at-once rewrite. I'd prioritize extracting the pieces causing the most genuine, currently-felt pain first, rather than extracting services in a purely arbitrary order.
I check whether its bounded context is genuinely clear and well-justified, whether it's actually solving a real organizational or technical need rather than splitting services just for the sake of splitting them, and whether the team proposing it has a realistic, credible plan for the added operational overhead, monitoring, deployment, on-call responsibility, that inevitably comes with any additional new service.
Automate what can genuinely be automated, contract testing, consistent structured logging requirements, enforced directly in each team's own CI pipeline, so standards aren't purely a matter of individual opinion in code review. For the architectural decisions that genuinely resist full automation, I'd document the handful of principles that actually matter most, with the concrete reasoning clearly behind each one, rather than a long, exhaustive governance document nobody actually reads end to end.
Watch for concrete warning signs like a single typical business operation requiring calls across an excessive number of separate services, teams routinely struggling to reliably trace a request's full path across the system, or the sheer operational overhead of running so many separate services clearly starting to outweigh any real, remaining architectural benefit. Consolidating some services back together, sometimes described as forming a more sensibly-sized macroservice, is a genuinely legitimate and sometimes necessary correction, not an admission of failure.
Distributed tracing is the actual, essential starting point here, since it shows precisely where time is genuinely being spent across a full, complete request's path, rather than requiring you to guess or manually investigate one service at a time. A shared dependency, like a common database or a shared downstream service that many other services all happen to call, is a very common actual root cause, since a slowdown there genuinely radiates outward and affects everything that ultimately depends on it, even indirectly.
Standardize on shared, common metrics and structured logging formats across every single service, so a central monitoring and observability platform can meaningfully aggregate and correlate data across the entire system, rather than each individual team building its own separate, incompatible, siloed monitoring approach independently. I'd also make sure alerting genuinely accounts for real service dependencies, so a single root-cause failure doesn't trigger dozens of separate, seemingly unrelated alerts across every service that happens to depend on it.
Treat the API or library's public interface as a genuine contract with every consuming team. Additive changes are generally safe. Changing or removing something that already exists needs a documented deprecation period and direct communication with every consuming team well ahead of time, rather than a silent breaking change that quietly surfaces later as someone else's completely unrelated-seeming production incident.
Mitigate first: identify and isolate the actual originally-failing service, and specifically check whether circuit breakers and bulkheads genuinely worked as intended to contain that failure, or whether they were actually missing or misconfigured, which is very often the real reason a single failure was allowed to cascade this far in the first place. After the immediate incident is genuinely resolved, add or properly fix the specific resilience patterns that clearly should have contained that failure before it ever cascaded this widely.
Load test each individual service independently, and specifically identify which particular services are genuinely likely to become the actual bottleneck first, rather than assuming every single service scales identically or proportionally to the others under the same increase. I'd prioritize scaling and closely monitoring those specific bottleneck services first, rather than uniformly over-provisioning every single service in the system equally, regardless of its actual individual load profile.
This is a judgment question interviewers use to see how you reason under genuine uncertainty, not to test a specific textbook fact. A strong answer names the actual constraint that forced the decision, the realistic options that were genuinely on the table, why you picked one knowing it wasn't guaranteed to be right, and what you'd do differently with what you know now, since architecture decisions in a distributed system are almost always made with a genuinely incomplete picture of exactly how the system will actually behave once it's fully deployed and under real, live traffic.
I'd walk through a real, concrete example of a design decision they actually made and trace its genuine, actual downstream effects on other services and teams, since that broader, system-level thinking often only develops once someone has directly experienced a decision's real, concrete ripple effects firsthand, rather than being told about them abstractly. Making that connection explicit and concrete tends to shift their instincts meaningfully more than a general reminder to think about the bigger picture ever does.
I wouldn't lead with a purely architectural or theoretical argument about optimal service sizing. I'd point to a specific, already-felt, concrete pain point, the genuine operational burden of managing so many separate services, or a specific incident that was made noticeably harder to properly diagnose because of how fragmented the system had actually become, and let that already-experienced, concrete cost make the actual case rather than arguing for consolidation purely in the abstract.
I'd bring the actual, concrete use cases and specific data behind each side's position, rather than a general, abstract preference for one particular API design over another. Most disagreements like this genuinely resolve once both teams are looking at the exact same concrete requirements and constraints together, instead of arguing from each team's own differing, unstated assumptions about what the other team actually needs.
I'd translate the shared investment into terms each team already cares about directly: hours currently spent by each team independently reimplementing the same retry logic or logging setup, and the reduced time to actually diagnose a real incident once tracing and metrics are genuinely standardized across every service. Framed as time each team gets back, rather than a purely centralized architectural mandate imposed from outside, it tends to get real, genuine buy-in far more easily.




