Microservices Interview Questions and Answers for Senior Java Engineers
Microservices interview questions for senior Java engineers test whether you can design, run and evolve a system of independently deployable services, and explain the trade-offs behind each decision. At senior level, a correct definition is the starting point. Interviewers listen for failure modes, consistency guarantees, operational experience and the judgement to say when not to use a pattern.
This page collects 50 questions that come up again and again in senior and lead interviews, with model answers, follow-up hints, four runnable Java 21 programs and a set of system design scenarios. Every answer is written to be said out loud in about one to two minutes.
1. What Interviewers Expect From Senior Engineers
A microservice is a service that owns one business capability and its data, and can be deployed without redeploying the rest of the system. Nearly every hard question follows from one fact: there is now a network between your components. Calls can be slow, fail halfway or arrive twice, and no single database transaction covers the whole business operation.
The same question is scored differently by level. Take "How do you keep data consistent across services?":
- A mid-level answer explains the Saga pattern with compensating actions.
- A senior answer adds the dual-write problem and the outbox, idempotent consumers, what the user sees while the saga runs, and how stuck sagas are detected and repaired.
- A lead or architect answer also asks whether the boundary is right: if two services always need a transaction together, they may belong in one service.
1.1 A Structure for Every Answer
- Define the concept in one sentence.
- Name the problem it solves.
- Explain the mechanism: states, steps, guarantees.
- State the trade-off and when you would not use it.
- Give a real example, ideally with numbers: "p99 dropped from 2.1 s to 300 ms".
2. Architecture and Service Boundaries
Q. When would you advise against microservices?
Answer: When the team is small, the domain is still being discovered, or the organisation cannot yet deploy, monitor and debug many services. Microservices trade code complexity for operational complexity: network failures, distributed data, versioning and many more deployable units. A modular monolith with strict module boundaries often gives most of the benefit. Modules can be extracted later, once the boundaries have proven stable and a real need appears, such as independent scaling or independent release cycles.
Q. How do you find good service boundaries?
Answer: Start from business capabilities and bounded contexts from Domain-Driven Design: areas in which a term has one clear meaning. "Product" in the catalogue (images, descriptions) is a different model from "product" in the warehouse (weight, shelf location). Then check the boundary with practical tests: can the service be changed and deployed without coordinating with others? Does it own its data? Are most calls inside the boundary rather than across it?
Q. What is a distributed monolith, and how do you recognise one?
Answer: It is a system that is split into services but still has to be deployed together. Signs include: releases that need several services deployed in a fixed order, shared libraries containing domain models, a shared database, long synchronous call chains, and one team unable to ship without another. It has the costs of microservices and the coupling of a monolith. The fix is usually to merge services that change together, or to move to asynchronous, contract-based integration.
Q. How large should a microservice be?
Answer: Size is the wrong measure. A service should be small enough for one team to own and understand, and large enough that most business operations stay inside it. A useful test: if a typical feature changes three services, the boundaries are probably too fine. Very fine-grained "nanoservices" create chatty network calls and distributed transactions for no benefit.
Q. Walk me through a request in a typical microservices system.
Answer: Use a picture like the one below and walk it in order.
The gateway authenticates, rate-limits and routes. The order service calls payment synchronously because it needs the result, with a timeout and circuit breaker around the call. Each service writes only to its own database. The order service publishes OrderPlaced through an outbox, and the notification service consumes it on its own schedule. A trace ID flows through every hop, so the request can be followed in logs and traces.
Q. How do you migrate a monolith to microservices safely?
Answer: Use the Strangler Fig pattern. Put a routing layer in front of the monolith, extract one capability at a time, and move its routes to the new service. Start with a capability that has clear boundaries and real business value, not the most tangled one. The hardest part is data: the new service needs its own data, so you typically sync data from the monolith with change data capture during the transition, then switch ownership. Plan for both systems running side by side for months.
3. Communication, APIs and Idempotency
Q. How do you choose between synchronous and asynchronous communication?
Answer: Use synchronous calls when the caller needs the answer to continue, such as authorising a payment before confirming an order. Use asynchronous messaging when the caller doesn't need to wait, such as sending emails or updating search indexes. Asynchronous messaging removes temporal coupling: the consumer can be down without failing the producer. The cost is eventual consistency, harder debugging and the need for idempotent consumers.
Q. REST, gRPC or GraphQL?
Answer: REST over JSON for public APIs and simple resources, because every client and tool supports it. gRPC for internal calls that need low latency, strict contracts and streaming; it uses HTTP/2 and binary Protocol Buffers. GraphQL for client-facing APIs where different screens need different data shapes, often in a Backend for Frontend. The trade-offs to mention: gRPC is harder to call from browsers and to inspect, and GraphQL needs care with query cost limits, caching and N+1 resolver calls.
Q. What is idempotency, and how do you implement it for a payment API?
Answer: An operation is idempotent if doing it twice has the same effect as doing it once. Payments must be idempotent because clients retry after timeouts, and a timeout does not mean the first attempt failed. The client sends an idempotency key with each logical operation, and the server stores the key with the result and returns the stored result for repeats.
import java.math.BigDecimal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class IdempotencyDemo {
record PaymentRequest(String orderId, BigDecimal amount) {}
record Receipt(String paymentId, PaymentRequest request) {}
static class PaymentService {
private final Map<String, Receipt> byKey = new ConcurrentHashMap<>();
private final AtomicInteger charges = new AtomicInteger();
Receipt pay(String idempotencyKey, PaymentRequest request) {
// computeIfAbsent runs the charge at most once per key, even under concurrency
Receipt receipt = byKey.computeIfAbsent(idempotencyKey,
key -> new Receipt("pay-" + charges.incrementAndGet(), request));
if (!receipt.request().equals(request)) { // same key, different body: a client bug
throw new IllegalArgumentException("key " + idempotencyKey + " was used for a different request");
}
return receipt;
}
int charges() { return charges.get(); }
}
public static void main(String[] args) throws Exception {
PaymentService payments = new PaymentService();
PaymentRequest request = new PaymentRequest("order-7", new BigDecimal("49.90"));
// 1. The client times out and retries with the SAME key
Receipt first = payments.pay("key-A", request);
Receipt retry = payments.pay("key-A", request);
System.out.println("First call : " + first.paymentId());
System.out.println("Retry : " + retry.paymentId());
// 2. Five duplicate requests arrive at the same moment
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 5; i++) {
executor.submit(() -> payments.pay("key-B", request));
}
} // close() waits for all tasks
System.out.println("Charges after 5 parallel duplicates: " + payments.charges());
// 3. A buggy client reuses a key for a different amount
try {
payments.pay("key-A", new PaymentRequest("order-7", new BigDecimal("99.90")));
} catch (IllegalArgumentException e) {
System.out.println("Rejected : " + e.getMessage());
}
System.out.println("Total charges: " + payments.charges());
}
}
First call : pay-1
Retry : pay-1
Charges after 5 parallel duplicates: 2
Rejected : key key-A was used for a different request
Total charges: 2
There are only 2 charges for 8 calls. ConcurrentHashMap.computeIfAbsent runs the mapping function at most once per key, so five parallel duplicates create one charge. A reused key with a different body is rejected, which is how Stripe's API behaves too, because silently returning the old result would hide a client bug.
IN_PROGRESS status first, store the response when done, return 409 Conflict while a duplicate is still in progress, and expire keys after a set period. An in-memory map is lost on restart and isn't shared between instances.Q. What delivery guarantees do message brokers give, and what does "exactly once" really mean?
Answer: At-most-once may lose messages; at-least-once never loses them but may deliver duplicates; exactly-once means each message's effect happens once. Kafka offers exactly-once processing only within Kafka: an idempotent producer plus transactions, with consumers reading read_committed. As soon as the consumer writes to an external database or calls an API, you are back to at-least-once delivery plus idempotent processing. So in practice, "exactly once" means "at least once, with deduplication".
Q. How do you preserve message ordering?
Answer: Kafka guarantees order only within a partition. Use a key such as the order ID, so all events for one order go to the same partition and are processed in order. Ordering across different keys is not guaranteed and usually not needed. Watch for things that break ordering: retries that send a failed message to a separate retry topic while later messages continue, and consumers that process one partition's messages in parallel. Include a version number in events so consumers can ignore stale updates.
Q. What happens to messages that can never be processed?
Answer: A poison message, such as one with a broken payload, would block a partition forever if the consumer kept retrying it. After a limited number of retries, move it to a dead letter topic together with error details, alert on it, and continue. Provide tooling to inspect, fix and replay dead letters. Make sure replayed messages are still handled idempotently and that ordering-sensitive flows can cope with the delay.
4. Data Consistency and Transactions
Q. Why does each service need its own database, and what does that cost?
Answer: Data ownership is what makes independent deployment possible. If two services share tables, a schema change in one can break the other, and one service's heavy queries can slow the other down. "Own database" can mean a separate schema with restricted access, not necessarily a separate server. The costs are no cross-service joins, no cross-service ACID transactions, and data that is duplicated and eventually consistent.
Q. Why not use two-phase commit across services?
Answer: Two-phase commit (2PC) needs a coordinator, and every participant must hold locks until the coordinator decides. If the coordinator fails at the wrong moment, participants stay blocked. It also needs XA support, which many modern databases and almost all message brokers don't support well. It reduces availability and throughput, which are the reasons for choosing microservices in the first place. Sagas accept temporary inconsistency in exchange for availability.
Q. Explain the Saga pattern and show how compensation works.
Answer: A saga is a sequence of local transactions, one per service. If a step fails, the saga runs compensating actions for the steps already completed, in reverse order.
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
public class SagaDemo {
// Each step has a forward action and a compensating action that undoes its business effect
record Step(String name, Runnable action, Runnable compensation) {}
static void runSaga(String orderId, List<Step> steps) {
Deque<Step> done = new ArrayDeque<>();
for (Step step : steps) {
try {
step.action().run();
done.push(step); // remember completed steps, newest first
} catch (RuntimeException e) {
System.out.println(" FAILED " + step.name() + ": " + e.getMessage());
while (!done.isEmpty()) {
done.pop().compensation().run(); // undo in reverse order
}
System.out.println(" Order " + orderId + " -> CANCELLED");
return;
}
}
System.out.println(" Order " + orderId + " -> CONFIRMED");
}
static List<Step> placeOrder(boolean courierAvailable) {
return List.of(
new Step("reserve stock",
() -> System.out.println(" stock reserved: 2 x headphones"),
() -> System.out.println(" compensate: stock released")),
new Step("charge card",
() -> System.out.println(" card charged: 120.00"),
() -> System.out.println(" compensate: refund 120.00 issued")),
new Step("book courier",
() -> {
if (!courierAvailable) throw new IllegalStateException("no courier for postcode 40213");
System.out.println(" courier booked");
},
() -> System.out.println(" compensate: courier cancelled")));
}
public static void main(String[] args) {
System.out.println("Saga 1:");
runSaga("A-100", placeOrder(true));
System.out.println("Saga 2:");
runSaga("A-101", placeOrder(false));
}
}
Saga 1:
stock reserved: 2 x headphones
card charged: 120.00
courier booked
Order A-100 -> CONFIRMED
Saga 2:
stock reserved: 2 x headphones
card charged: 120.00
FAILED book courier: no courier for postcode 40213
compensate: refund 120.00 issued
compensate: stock released
Order A-101 -> CANCELLED
Compensation is not a rollback. The card was really charged, so the customer may see both the charge and the refund. That is why steps that are hard to compensate, or that the customer can see, are placed as late as possible, and steps likely to fail are placed early. In a real system each step is a remote call or message, so steps and compensations must be idempotent and retried until they succeed, and the saga state is stored so it can resume after a crash.
Q. Orchestration or choreography?
Answer: In orchestration, a coordinator (for example a state machine, or a workflow engine such as Temporal or Camunda) tells each service what to do and stores the saga state. It is easier to follow, test and monitor. In choreography, services react to each other's events with no central coordinator. It has lower coupling, but the overall flow is hard to see and cyclic dependencies appear easily. Choreography suits short flows of two or three steps; orchestration suits longer flows that need visibility, timeouts and manual intervention.
Q. What is the dual-write problem, and how does the transactional outbox solve it?
Answer: A service often needs to update its database and also publish an event. Doing these as two separate operations means a crash between them leaves them inconsistent: the order is saved but the event is lost, or the event is sent for an order that was rolled back. With the transactional outbox, the event is written to an outbox table in the same local transaction as the business change. A relay process, or change data capture with a tool like Debezium reading the database log, publishes the rows to the broker. This gives at-least-once publishing, so consumers must be idempotent.
Q. How do you make a consumer idempotent?
Answer: Give every event a unique ID. In the same database transaction as the business change, insert the event ID into a processed_events table with a unique constraint. A duplicate fails the insert and is skipped. Alternatively, make the operation naturally idempotent, for example "set status to SHIPPED" instead of "increment shipped count", or use version numbers so an old update can't overwrite a newer one.
Q. When is CQRS worth it?
Answer: CQRS separates the write model from one or more read models. It pays off when reads and writes differ greatly in shape or volume, such as a simple order write model but complex search and reporting screens, or when one query needs data owned by several services. The costs are eventual consistency between the models, more infrastructure, and the need to rebuild read models when they change. For simple CRUD it is overkill. CQRS does not require event sourcing, although the two are often used together.
Q. What are the trade-offs of event sourcing?
Answer: Event sourcing stores every change as an event and rebuilds the current state by replaying them. Benefits: a full audit history, the ability to build new read models from the past, and time-travel debugging. Costs: event schemas can never simply be changed, so you need upcasting or versioning; replaying long histories needs snapshots; querying the current state needs projections; and fixing bad data means writing corrective events, not running an UPDATE. Use it where the history itself is valuable, such as ledgers, not as a default.
5. Resilience and Fault Tolerance
Q. How does a cascading failure happen, and how do you stop it?
Answer: A slow dependency is often worse than a dead one. Callers wait, their threads and connections fill up, they become slow themselves, and the slowness spreads upward until everything is down. Stop it with layered defences:
- A timeout on every remote call.
- A circuit breaker to stop calling a failing dependency.
- A bulkhead to cap the resources one dependency can use.
- A fallback to degrade gracefully.
- Load shedding at the edge to reject excess work early.
Q. Implement a circuit breaker.
Answer:
import java.util.function.Supplier;
public class CircuitBreakerDemo {
enum State { CLOSED, OPEN, HALF_OPEN }
static class CircuitBreaker {
private final int failureThreshold;
private final long openMillis;
private State state = State.CLOSED;
private int failures;
private long openedAt;
long now; // a fake clock keeps the demo deterministic
CircuitBreaker(int failureThreshold, long openMillis) {
this.failureThreshold = failureThreshold;
this.openMillis = openMillis;
}
<T> T call(Supplier<T> action, Supplier<T> fallback) {
if (state == State.OPEN) {
if (now - openedAt < openMillis) return fallback.get(); // fail fast, no remote call
moveTo(State.HALF_OPEN); // wait is over: allow one trial
}
try {
T result = action.get();
failures = 0;
if (state == State.HALF_OPEN) moveTo(State.CLOSED);
return result;
} catch (RuntimeException e) {
failures++;
if (state == State.HALF_OPEN || failures >= failureThreshold) {
moveTo(State.OPEN);
openedAt = now;
}
return fallback.get();
}
}
private void moveTo(State next) {
System.out.println(" [" + state + " -> " + next + " at t=" + now + "ms]");
state = next;
}
}
static boolean inventoryUp = false;
static int remoteCalls = 0;
static String fetchStock() {
remoteCalls++;
if (!inventoryUp) throw new IllegalStateException("inventory service down");
return "12 in stock";
}
public static void main(String[] args) {
CircuitBreaker breaker = new CircuitBreaker(3, 5_000);
long[] times = {0, 100, 200, 300, 400, 5_400, 9_000, 10_500};
for (long t : times) {
breaker.now = t;
if (t == 10_500) inventoryUp = true; // the service has recovered
String answer = breaker.call(CircuitBreakerDemo::fetchStock, () -> "stock unknown (cached page)");
System.out.printf("t=%5dms %-28s remote calls so far: %d%n", t, answer, remoteCalls);
}
}
}
t= 0ms stock unknown (cached page) remote calls so far: 1
t= 100ms stock unknown (cached page) remote calls so far: 2
[CLOSED -> OPEN at t=200ms]
t= 200ms stock unknown (cached page) remote calls so far: 3
t= 300ms stock unknown (cached page) remote calls so far: 3
t= 400ms stock unknown (cached page) remote calls so far: 3
[OPEN -> HALF_OPEN at t=5400ms]
[HALF_OPEN -> OPEN at t=5400ms]
t= 5400ms stock unknown (cached page) remote calls so far: 4
t= 9000ms stock unknown (cached page) remote calls so far: 4
[OPEN -> HALF_OPEN at t=10500ms]
[HALF_OPEN -> CLOSED at t=10500ms]
t=10500ms 12 in stock remote calls so far: 5
After three failures the breaker opens, and calls at 300 ms and 400 ms are answered by the fallback with no network call. At 5,400 ms one trial call is allowed; it fails, and the breaker goes straight back to OPEN without needing three new failures. At 10,500 ms the trial succeeds and the breaker closes.
Q. How do you configure retries safely?
Answer: Retry only temporary errors, such as timeouts, connection resets or 503, and only idempotent operations. Use exponential backoff with jitter, cap the number of attempts and the total time, and keep the total within the caller's own timeout. Retry at one layer only: if the gateway, the service and the client library each retry three times, one request can become 27. A retry budget, for example retries limited to 10% of requests, stops retries from multiplying load during an outage.
Q. How do you choose timeout values?
Answer: Base them on measured latency, not guesses: start a little above the dependency's p99 latency. Then use a deadline: the remaining time budget from the original request is passed downstream, so a service doesn't keep working on a request whose caller has already given up. gRPC propagates deadlines natively; with HTTP you pass the remaining budget in a header. Also set connect and read timeouts separately, and check that the connection-pool acquire timeout isn't left unlimited.
Q. Fetch data from several services in parallel, with a time budget.
Answer: Product pages, dashboards and API composition all fan out to several services. Required data should fail fast, while optional data should fall back.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class FanOutDemo {
static final AtomicBoolean reviewsFinished = new AtomicBoolean();
static String remoteCall(String service, long millis) {
try {
Thread.sleep(millis); // stands in for an HTTP call
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (service.equals("reviews")) reviewsFinished.set(true);
return service + " data";
}
public static void main(String[] args) {
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
long start = System.nanoTime();
// Required data: fail the page if it is late
CompletableFuture<String> product = CompletableFuture
.supplyAsync(() -> remoteCall("product", 50), executor)
.orTimeout(300, TimeUnit.MILLISECONDS);
CompletableFuture<String> price = CompletableFuture
.supplyAsync(() -> remoteCall("price", 80), executor)
.orTimeout(300, TimeUnit.MILLISECONDS);
// Optional data: use a fallback if it is late
CompletableFuture<String> reviews = CompletableFuture
.supplyAsync(() -> remoteCall("reviews", 2_000), executor)
.completeOnTimeout("reviews hidden (fallback)", 300, TimeUnit.MILLISECONDS);
String page = String.join(" | ", product.join(), price.join(), reviews.join());
long millis = (System.nanoTime() - start) / 1_000_000;
System.out.println("Page: " + page);
System.out.println("Built within 500 ms budget: " + (millis < 500));
System.out.println("Slow reviews call already finished? " + reviewsFinished.get());
} // close() waits for every task, including the slow reviews call
System.out.println("After executor close, reviews call finished? " + reviewsFinished.get());
}
}
Page: product data | price data | reviews hidden (fallback)
Built within 500 ms budget: true
Slow reviews call already finished? false
After executor close, reviews call finished? true
The page was built in time, but look at the last two lines: completeOnTimeout does not stop the slow call. It only completes the future with the fallback value; the reviews call kept running in the background until it finished. Under load, those abandoned calls still use connections and threads on both sides. Real clients should also use their own HTTP timeouts, so the underlying request is actually aborted.
Q. What is the bulkhead pattern, and how does it work with virtual threads?
Answer: A bulkhead limits the concurrent calls to one dependency, so it can't use up all shared resources. With platform threads it is often a separate fixed thread pool per dependency. With virtual threads, threads are cheap, so the thread count no longer protects you; instead you limit concurrency with a semaphore, and you must still size connection pools carefully, because they become the real limit.
Q. What is backpressure, and why does it matter?
Answer: Backpressure is a way for a slow consumer to tell a fast producer to slow down. Without it, work queues up in memory until latency explodes or the service runs out of memory. In practice: bounded queues, rejecting work with 429 or 503 when full, consumer-controlled pulling with Kafka's max.poll.records, and reactive streams in Project Reactor. An unbounded queue just hides overload until it becomes an outage.
6. Observability and Production Operations
Q. How do you debug a slow request across 20 services?
Answer: Start from metrics: the latency of the entry point and when it changed, and whether that matches a deployment. Then use distributed tracing: open slow traces and find the span that takes the time. The trace ID is passed between services in the W3C traceparent header, so every log line for that request can be found by trace ID. Finally, check that service's own metrics: connection-pool wait time, GC pauses, CPU throttling and its downstream calls. In Spring Boot 3, Micrometer Tracing with an OpenTelemetry bridge handles propagation and adds trace IDs to logs.
Q. What would you alert on?
Answer: Alert on user-visible symptoms, not causes: error rate, latency against the SLO, and saturation. A common approach is to define SLOs (for example, 99.9% of checkout requests succeed within 500 ms) and alert on the error budget burn rate, so a fast burn pages someone immediately and a slow burn creates a ticket. CPU above 80% on its own should not wake anyone up. For message-driven services, alert on consumer lag and the age of the oldest unprocessed message.
Q. What is the difference between liveness, readiness and startup probes?
Answer: Liveness asks whether the process is stuck; Kubernetes restarts it when the check fails. Readiness asks whether it can serve traffic now; Kubernetes removes it from the Service endpoints, but doesn't restart it. Startup protects slow-starting apps from being killed by liveness checks during startup. Never put a database check in liveness: a database outage would cause every pod to restart, making things worse.
Q. How do you shut down a service without dropping requests?
Answer: When Kubernetes terminates a pod, removing it from the endpoints and sending SIGTERM happen in parallel, so the pod may still receive new requests for a few seconds. Add a short preStop sleep, then let the application drain: Spring Boot's graceful shutdown (the default since Spring Boot 3.4) stops accepting new requests and waits for active ones, up to spring.lifecycle.timeout-per-shutdown-phase, which defaults to 30 seconds. Message consumers should stop polling and commit offsets. Keep the total under terminationGracePeriodSeconds, which also defaults to 30 seconds.
Q. How do you handle configuration and secrets?
Answer: Keep configuration outside the build artefact, so the same image runs in every environment. Use environment variables, Kubernetes ConfigMaps or a config server. Secrets belong in a secret manager such as Vault or a cloud secret manager, with short-lived credentials where possible, never in images or Git. Treat configuration changes like code: review them, version them, and roll them out gradually, because a bad config change can break production as easily as a bad deployment.
7. Security
Q. How do you design authentication and authorisation across services?
Answer: Users authenticate with an identity provider through OAuth 2.0 and OpenID Connect. The gateway validates the access token, usually a JWT, and passes it on. Each service validates the token again and makes its own authorisation decisions, because it is the only one that knows its data rules, such as "a customer can only see their own orders". Coarse checks belong at the gateway; fine-grained, data-level checks belong in the service.
Q. How do services authenticate to each other?
Answer: Use mutual TLS, where both sides present certificates. A service mesh like Istio or Linkerd can issue and rotate them automatically. For calls made on a user's behalf, propagate the user's token or exchange it for a narrower one (OAuth 2.0 Token Exchange, RFC 8693). For calls made by the service itself, use the client credentials flow. Follow zero trust: being inside the network is not proof of identity.
Q. JWTs can't be revoked easily. How do you handle that?
Answer: A JWT is validated locally with the issuer's public key, so a stolen token works until it expires. Keep access tokens short-lived, for example 5 to 15 minutes, and use refresh tokens that can be revoked at the identity provider. For urgent cases, keep a small deny-list of token IDs (jti) in a fast store. Validate iss, aud, exp and the algorithm, and cache the issuer's public keys (JWKS) with rotation in mind.
Q. What security risks are specific to microservices?
Answer: A much larger attack surface: many endpoints, many dependencies and many credentials. Watch for internal APIs exposed by mistake, missing authorisation checks in internal services (broken object-level authorisation), secrets leaking into logs or traces, unvalidated data from events, and vulnerable dependencies multiplied across services. Defences include gateway policies, mTLS, central dependency scanning, signed images, and log masking for personal data.
8. Deployment, Testing and API Evolution
Q. Compare rolling, blue-green and canary deployments.
Answer: Rolling replaces instances a few at a time and is the Kubernetes Deployment default. Blue-green prepares a full new environment and switches traffic at once, which makes rollback instant but doubles capacity during the switch. Canary sends a small, growing share of traffic to the new version and promotes it only if metrics stay healthy, which limits the impact of a bad release. All three require backward-compatible database and API changes, because old and new versions run at the same time.
Q. How do you change a database schema with zero downtime?
Answer: Use expand and contract. First expand: add the new column or table without removing anything, and deploy code that writes to both the old and new structures. Backfill existing data. Then switch reads to the new structure. Only when no running version uses the old structure, contract by removing it. Each step must work with both the previous and the next version of the code, and tools like Flyway or Liquibase keep the steps ordered.
Q. How do you test microservices without a fragile end-to-end environment?
Answer: Use a test pyramid adapted to services. Unit tests cover logic. Integration tests with Testcontainers use a real database and broker. Consumer-driven contract tests, for example with Pact or Spring Cloud Contract, check that a provider still meets what its consumers expect, without starting every service. Keep a few end-to-end tests for critical journeys only. Add resilience testing with injected faults, using tools such as Toxiproxy, to check timeouts and fallbacks.
Q. How do you evolve APIs and event schemas without breaking consumers?
Answer: Prefer backward-compatible changes: add optional fields, never rename or remove fields, and make consumers tolerate unknown fields (the tolerant reader). For events, use a schema registry with a compatibility rule; Confluent Schema Registry defaults to BACKWARD, meaning new consumers can read old data. For breaking REST changes, run a new version alongside the old one, track who still calls the old version, and remove it only when usage reaches zero.
9. Java and Spring Boot in Microservices
Q. Should microservices use virtual threads?
Answer: Virtual threads (final in Java 21) make blocking I/O cheap, so a simple thread-per-request style can handle high concurrency without reactive code. In Spring Boot 3.2 and later, spring.threads.virtual.enabled=true turns them on for request handling. Three cautions: connection pools, not threads, become the limit; in Java 21, blocking inside a synchronized block pins the carrier thread (fixed in JDK 24 by JEP 491); and ThreadLocal caches that assumed few threads can use a lot of memory.
Q. Reactive (WebFlux) or blocking with virtual threads?
Answer: With virtual threads, the blocking style now gets most of the scalability benefit for I/O-heavy services, and it is easier to write, debug and profile. Reactive still fits well for streaming, backpressure across the whole pipeline, and services already built on Reactor. Spring Cloud Gateway is reactive by default, and there is now also a Server MVC variant. A senior answer chooses based on the team's skills and the workload, not on fashion.
Q. How do you size the database connection pool?
Answer: Not by matching the thread count. A database can only do a limited amount of work in parallel, and more connections often make it slower. Start with HikariCP's default of 10 per instance and size it from measurements. Remember that the total is pool size × number of instances: 40 pods with 20 connections each is 800 connections, which can exceed the database limit when autoscaling kicks in. Little's law helps: connections needed ≈ queries per second × average query time in seconds.
Q. Which HTTP client should a Spring Boot service use?
Answer: For new blocking code, RestClient (Spring Framework 6.1, Spring Boot 3.2), or declarative HTTP interfaces with @HttpExchange. RestTemplate is in maintenance mode. WebClient fits reactive applications. Whatever you choose, configure connect and read timeouts explicitly, set connection-pool limits, add resilience such as Resilience4j around calls, and make sure observability instruments the client so trace IDs are propagated.
Q. How do you reduce startup time and memory for Java services?
Answer: Options include: class data sharing (AppCDS) for faster class loading; GraalVM native images with Spring Boot AOT for fast startup and low memory, at the cost of longer builds and reflection limits; CRaC checkpoint and restore where the platform supports it; and Project Leyden's ahead-of-time features in newer JDKs. Also look at the basics: fewer dependencies, lazy initialisation where it is safe, and right-sized container memory with -XX:MaxRAMPercentage.
Q. Feign, Eureka and Spring Cloud Config: still needed on Kubernetes?
Answer: Often not. Kubernetes Services and DNS replace client-side discovery with Eureka for most cases, ConfigMaps and Secrets replace much of a config server, and a service mesh can handle mTLS and some traffic policies. Spring Cloud is still useful outside Kubernetes, for client-side load-balancing features, or when teams want portability across platforms. The senior answer is to avoid running two systems that do the same job.
10. System Design Scenarios
Q. Design checkout for an online shop.
Answer: Clarify requirements first: order volume, peak traffic, payment providers, and whether stock must be reserved. A good design has an order service that creates the order as PENDING and starts an orchestrated saga: reserve stock, authorise payment, confirm the order, and capture payment at shipping. Use idempotency keys from the client, an outbox for events, and a timeout on every step with compensation. The user sees "order received" immediately and gets the confirmation asynchronously. Discuss failure cases: payment provider timeouts (query the status before retrying), a stuck saga (a timeout handler and a dashboard), and duplicate submissions (the idempotency key).
Q. A flash sale: 100 items, 1 million users at the same second. How do you avoid overselling?
Answer: The stock counter is a single hot spot, so protect it. Options are an atomic conditional update (UPDATE stock SET qty = qty - 1 WHERE id = ? AND qty > 0), an atomic decrement in Redis with the database as the system of record, or a queue that serialises purchase requests per item. Put a virtual waiting room or rate limiter in front to shed load early, and give each user a short reservation that expires if not paid. Avoid a distributed lock as the main mechanism unless you also use fencing tokens, because a paused lock holder can still write after its lock has expired.
Q. Customers sometimes get two confirmation emails. What is happening, and how do you fix it?
Answer: With at-least-once delivery, a message is redelivered when a consumer crashes or a Kafka consumer group rebalances before the offset is committed. The email consumer is not idempotent. Fix it by recording each event ID in a processed-events table before sending, or use the email provider's idempotency key if it has one. Also check the producer side: a retry without an outbox can publish the same event twice with different IDs, so include a stable business key, such as the order ID plus the event type.
Q. After a release, p99 latency doubled, but average latency barely changed. How do you investigate?
Answer: A change in the tail but not in the average points to something that affects a subset of requests: a slow dependency for some request types, lock contention, GC pauses, connection-pool waits, or one bad pod. Compare p99 by endpoint, by pod and by downstream dependency. Look at slow traces from before and after the release, and check GC logs and pool metrics. If the release is the clear cause, roll back first and investigate afterwards; protecting users comes before finding the root cause.
11. Commonly Confused Concepts
| Pair | First one | Second one | Key difference |
|---|---|---|---|
| Orchestration vs choreography | A coordinator drives each step | Services react to events | Visibility and control vs lower coupling |
| 2PC vs saga | All participants lock and commit together | Local commits with compensation | Strong consistency vs availability |
| Outbox vs CDC | A pattern: events stored in a table in the same transaction | A technique: reading the database log | CDC is one way to publish an outbox |
| API gateway vs service mesh | Traffic entering the system (north-south) | Traffic between services (east-west) | Many systems use both |
| Timeout vs circuit breaker | Limits one call | Reacts to many failed calls | Timeouts produce the failures a breaker counts |
| Rate limiter vs bulkhead | Calls per time period | Calls running at the same moment | Slow calls can fill a bulkhead under a rate limit |
| Liveness vs readiness | Restart if stuck | Stop routing traffic if not ready | Never put dependencies in liveness |
| Blue-green vs canary | Switch all traffic at once | Shift traffic gradually | Instant rollback vs limited impact |
12. Mistakes Senior Candidates Make
1. Answering with tools instead of concepts. "We used Eureka" is not an explanation of service discovery. Explain the mechanism, then name tools as examples.
2. Not asking clarifying questions in design rounds. Scale, consistency needs and failure tolerance change the design. Jumping straight to Kafka and Kubernetes sounds junior.
3. Treating timeouts as failures. If a payment call times out, the payment may have succeeded. Mention idempotency keys and status checks.
4. Ignoring the in-between state. With eventual consistency, there is a time when services disagree. Say what the user sees and how stuck states are repaired.
5. Claiming exactly-once delivery end to end. It holds only inside specific boundaries. The safe answer is at-least-once plus idempotent processing.
6. Retrying everywhere. Retries at every layer multiply load during an outage. Name the layer that owns retries and the budget.
7. No operational story. Seniors are expected to have been on call. Have one incident story ready: what broke, how you found it, and what you changed.
13. Preparation Checklist
- Write the four programs above yourself without looking: idempotent endpoint, circuit breaker, saga and parallel fan-out with timeouts.
- Draw the reference architecture and explain what happens when each box fails.
- Prepare three stories: a production incident, a design decision you would change today, and a time you argued against adding a service or technology.
- Know the numbers: your services' request rates, p99 latencies, team size, number of services and deployment frequency.
- Know one stack in depth, for example Spring Boot 3, Resilience4j, Kafka, Micrometer with OpenTelemetry, and Kubernetes, including key defaults.
- Practise design rounds out loud: requirements, then the high-level design, then the data model, then failure modes, then trade-offs.
- Say "it depends" only if you then say what it depends on.
14. Summary
- Microservices interview questions for senior engineers test trade-offs and failure modes, not definitions.
- Data consistency is the core topic: sagas, the outbox, idempotent consumers and what the user sees in between.
- Resilience means layered defences: timeouts with deadlines, circuit breakers, bulkheads, safe retries and fallbacks.
- Observability and operations matter: tracing, SLO-based alerts, probes and graceful shutdown.
- Know the Java specifics: virtual threads and pinning, connection pool sizing, modern Spring HTTP clients and startup options.
- In design scenarios, clarify requirements, then walk through failure cases.
Work through these microservices interview questions out loud, write the code yourself, and connect each answer to something you have built or operated. That combination is what interviewers look for at senior level.