34 Microservices Interview Questions (2026)

·31 min read
By ·Updated
microservicesspring-clouddistributed-systemsarchitecturejavainterview-preparation

Microservices architecture is one of those topics where the interview reveals whether you've actually built distributed systems or just read about them. Anyone can recite the definition - "independently deployable services organized around business capabilities." The real questions probe deeper: How do you handle a transaction spanning three services? What happens when the payment service is slow? How do you debug a request that touches twelve services?

This guide covers microservices at the depth interviewers expect from senior developers. Not just patterns and definitions, but the trade-offs, failure modes, and practical decisions that matter in production.

Table of Contents

  1. Microservices Fundamentals Questions
  2. Monolith vs Microservices Questions
  3. Service Communication Questions
  4. API Gateway Questions
  5. Service Discovery Questions
  6. Circuit Breaker Questions
  7. Resilience Pattern Questions
  8. Database Per Service Questions
  9. Saga Pattern Questions
  10. CQRS Questions
  11. Spring Cloud Questions
  12. Deployment and Observability Questions
  13. Microservices Testing Questions

Microservices Fundamentals Questions

Understanding the core principles of microservices helps you make better architectural decisions.

What are microservices and what problems do they solve?

Microservices are an architectural style where an application is built as a collection of small, autonomous services, each running in its own process and communicating through lightweight mechanisms like HTTP or messaging. Each service is independently deployable, scalable, and owned by a small team.

The key problems microservices solve include organizational scaling (multiple teams can work independently), technical scaling (different components can scale separately), and technology flexibility (each service can use the best tool for its job). However, they introduce distributed system complexity that must be carefully managed.

Key characteristics:

CharacteristicDescription
Single responsibilityEach service does one thing well
Independent deploymentDeploy without coordinating with other services
Decentralized dataEach service owns its data store
Smart endpoints, dumb pipesBusiness logic in services, not middleware
Design for failureAssume network is unreliable
Evolutionary designServices can be rewritten/replaced

What are bounded contexts and how do they relate to microservices?

Bounded contexts come from Domain-Driven Design (DDD) and represent areas where a particular domain model applies consistently. Within a bounded context, terms have specific meanings that may differ from other contexts. For example, "Product" in a Catalog context means something different than "Product" in a Shipping context.

Services should align with bounded contexts because this creates natural service boundaries. When you decompose by bounded context, each service has a coherent domain model, clear ownership, and minimal need to coordinate with other services for its core functionality.

flowchart TB
    subgraph Domain["E-Commerce Domain"]
        direction TB
        subgraph Catalog["Catalog Context"]
            C1["Product"]
            C2["Category"]
            C3["Price"]
        end
        subgraph Orders["Orders Context"]
            O1["Order"]
            O2["LineItem"]
            O3["Customer"]
        end
        subgraph Shipping["Shipping Context"]
            S1["Shipment"]
            S2["Tracking"]
            S3["Address"]
        end
    end
 
    Note["'Product' means different things in each context"]
 
    style Domain fill:#1e1b4b,stroke:#a855f7,stroke-width:2px
    style Catalog fill:#6366f1,stroke:#a855f7
    style Orders fill:#6366f1,stroke:#a855f7
    style Shipping fill:#6366f1,stroke:#a855f7
    style Note fill:#374151,stroke:#a855f7,stroke-width:1px

Monolith vs Microservices Questions

Choosing between monolith and microservices is one of the most important architectural decisions.

What is the difference between monolithic and microservices architecture?

A monolithic architecture packages all functionality into a single deployable unit with a shared database, while microservices split functionality into independent services, each with its own database. In a monolith, all components scale together and share the same technology stack. In microservices, components scale independently and can use different technologies.

The key trade-off is simplicity versus flexibility. Monoliths are simpler to develop, test, and deploy initially, but become harder to maintain as they grow. Microservices offer more flexibility and scalability but introduce distributed system complexity from day one.

Monolithic architecture:

flowchart TB
    subgraph Monolith["Monolith Application"]
        direction TB
        Users["Users"]
        Orders["Orders"]
        Payments["Payments"]
        DB[("Shared Database")]
        Users & Orders & Payments --> DB
    end
 
    style Monolith fill:#1e1b4b,stroke:#a855f7,stroke-width:2px
    style Users fill:#6366f1,stroke:#a855f7
    style Orders fill:#6366f1,stroke:#a855f7
    style Payments fill:#6366f1,stroke:#a855f7
    style DB fill:#7c3aed,stroke:#a855f7

Microservices architecture:

flowchart TB
    subgraph Services["Microservices"]
        direction LR
        US["Users Service"]
        OS["Orders Service"]
        PS["Payments Service"]
        US <--> OS <--> PS
    end
 
    subgraph Databases["Independent Databases"]
        direction LR
        UDB[("Users DB")]
        ODB[("Orders DB")]
        PDB[("Payments DB")]
    end
 
    US --> UDB
    OS --> ODB
    PS --> PDB
 
    style Services fill:#1e1b4b,stroke:#a855f7,stroke-width:2px
    style Databases fill:#242833,stroke:#a855f7,stroke-width:2px
    style US fill:#6366f1,stroke:#a855f7
    style OS fill:#6366f1,stroke:#a855f7
    style PS fill:#6366f1,stroke:#a855f7
    style UDB fill:#7c3aed,stroke:#a855f7
    style ODB fill:#7c3aed,stroke:#a855f7
    style PDB fill:#7c3aed,stroke:#a855f7

When should you choose microservices over a monolith?

The decision to adopt microservices should be driven by organizational and technical needs, not by following trends. Microservices make sense when the benefits of independent deployment and scaling outweigh the complexity costs of distributed systems.

Premature decomposition creates expensive boundaries before the domain is understood. A well-structured modular monolith can make later extraction easier when specific scaling, ownership, or release pain appears, though extraction is still a deliberate migration rather than an automatic outcome.

Choose microservices when:

  • Multiple teams need to deploy independently
  • Different components have vastly different scaling needs
  • You need technology diversity (Python for ML, Java for transactions)
  • The domain is complex enough to warrant bounded contexts
  • You have DevOps maturity (CI/CD, monitoring, container orchestration)

Stick with monolith when:

  • Small team (< 10 developers)
  • Simple domain
  • Unclear service boundaries
  • Limited DevOps capability
  • Startup exploring product-market fit

Service Communication Questions

How services communicate is fundamental to microservices design.

What are the different ways microservices can communicate?

Microservices commonly use synchronous request-response and asynchronous messaging. Synchronous communication (REST, gRPC) requires both services to be available during the call and adds to end-to-end latency. Messaging can decouple producer and consumer lifecycles, although publishing can still wait for broker acknowledgement or backpressure.

The choice depends on your consistency and latency requirements. Use synchronous for operations needing immediate response (checking inventory before confirming order). Use asynchronous for operations that can be processed later (sending confirmation email after order placed).

AspectSynchronousAsynchronous
CouplingTemporal coupling (both must be available)Decoupled (producer doesn't wait)
CompletionResult returned in the callResult observed through a later message or query
ConsistencyCan still be stale or eventually consistentOften eventual, depending on the workflow
Failure handlingTimeouts and errors returned to callerRetries, idempotency and poison-message handling
DebuggingDirect call chain, still needs correlationNeeds correlation across message boundaries

How do you implement REST communication between microservices in Spring?

Spring provides several options for HTTP communication: RestClient for synchronous calls, WebClient for non-blocking reactive calls, HTTP Service Clients for annotated interfaces, and Spring Cloud OpenFeign for declarative clients. As of Spring Framework 7, RestTemplate is deprecated in favor of RestClient.

Choose RestClient for a conventional blocking application and WebClient when the call path is reactive or streaming. Declarative clients reduce boilerplate, but a remote call must still expose timeouts, failures, and latency rather than pretending to be a local method.

// Using RestClient (synchronous)
@Service
public class OrderService {
    private final RestClient restClient;
 
    public UserDTO getUser(Long userId) {
        return restClient.get()
            .uri("http://user-service/api/users/{id}", userId)
            .retrieve()
            .body(UserDTO.class);
    }
}
 
// Using WebClient (reactive)
@Service
public class OrderService {
    private final WebClient webClient;
 
    public Mono<UserDTO> getUser(Long userId) {
        return webClient.get()
            .uri("http://user-service/api/users/{id}", userId)
            .retrieve()
            .bodyToMono(UserDTO.class);
    }
}
 
// Using OpenFeign (declarative)
@FeignClient(name = "user-service")
public interface UserClient {
 
    @GetMapping("/api/users/{id}")
    UserDTO getUser(@PathVariable Long id);
 
    @PostMapping("/api/users")
    UserDTO createUser(@RequestBody CreateUserRequest request);
}

When should you use gRPC instead of REST?

gRPC is an RPC framework that commonly uses Protocol Buffers and HTTP/2. Its generated contracts, streaming support, binary messages, and multiplexed connections can work well for internal APIs. Do not assume it is always faster than a REST endpoint: benchmark the actual payload, network, serialization, and client behavior.

The trade-off is ecosystem and operational complexity. REST is easy to inspect with common HTTP tools, while gRPC usually involves schema and code generation plus gRPC-aware tooling. Browsers need a compatible bridge or protocol such as gRPC-Web rather than a native gRPC/HTTP2 connection. The boundary should follow client compatibility and workload needs, not an external-versus-internal rule.

// user.proto
syntax = "proto3";
 
service UserService {
    rpc GetUser (GetUserRequest) returns (User);
    rpc CreateUser (CreateUserRequest) returns (User);
}
 
message GetUserRequest {
    int64 id = 1;
}
 
message User {
    int64 id = 1;
    string email = 2;
    string name = 3;
}
// gRPC client
@Service
public class OrderService {
    private final UserServiceGrpc.UserServiceBlockingStub userStub;
 
    public User getUser(long userId) {
        GetUserRequest request = GetUserRequest.newBuilder()
            .setId(userId)
            .build();
        return userStub.getUser(request);
    }
}
ProtocolUse When
RESTExternal APIs, simple CRUD, wide compatibility
gRPCInternal services, high throughput, streaming
GraphQLClient needs flexible queries, multiple frontends

How do you implement asynchronous communication with message queues?

Asynchronous communication uses brokers such as Kafka or RabbitMQ to decouple processing in time. A producer need not wait for a consumer to finish, but it may wait for the broker to acknowledge durable publication. Recovery depends on retention, retry, acknowledgement, ordering, and dead-letter policies rather than on messaging alone.

Spring Cloud Stream provides binder abstractions over supported brokers, but broker semantics still leak into delivery guarantees and configuration. Events should be immutable facts such as OrderCreated; commands such as CreateOrder are also valid messages when they target a single responsible consumer. Use stable schemas and idempotent consumers.

flowchart LR
    OS["Order Service"] --> MQ["Message Queue"]
    MQ --> IS["Inventory Service"]
    MQ --> SS["Shipping Service"]
 
    style OS fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style MQ fill:#7c3aed,stroke:#a855f7,stroke-width:2px
    style IS fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style SS fill:#6366f1,stroke:#a855f7,stroke-width:2px
// Publishing events (Spring Cloud Stream / Kafka)
@Service
public class OrderService {
 
    private final OrderOutboxRepository outboxRepository;
 
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        Order order = orderRepository.save(new Order(request));
 
        // Store the event in the same database transaction as the order.
        // A separate relay publishes committed outbox rows to the broker.
        OrderCreatedEvent event = new OrderCreatedEvent(
            order.getId(),
            order.getUserId(),
            order.getItems()
        );
        outboxRepository.save(OutboxMessage.from(event));
 
        return order;
    }
}
 
// A production relay publishes committed rows and marks them delivered.
// Consumers must remain idempotent because relay retries can create duplicates.
 
// Consuming events
@Component
public class InventoryEventHandler {
 
    @KafkaListener(topics = "orders")
    public void handleOrderCreated(OrderCreatedEvent event) {
        for (OrderItem item : event.getItems()) {
            inventoryService.reserve(item.getProductId(), item.getQuantity());
        }
    }
}

API Gateway Questions

An API Gateway provides a controlled entry point for external clients; larger systems may use multiple gateways or backends-for-frontends.

What is an API Gateway and what are its responsibilities?

An API Gateway is a server that acts as an entry point for client requests. Instead of exposing every microservice directly, the system routes appropriate calls through one or more gateways. This can centralize selected cross-cutting concerns and simplify client code without turning the gateway into a business-logic bottleneck.

The gateway handles responsibilities that would otherwise be duplicated across services: authentication, rate limiting, request routing, protocol translation, and response aggregation. It also provides a stable API even as backend services evolve, acting as a facade that shields clients from internal complexity.

flowchart TB
    Clients["Clients"]
    Gateway["API Gateway"]
    US["Users Service"]
    OS["Orders Service"]
    PS["Products Service"]
 
    Clients --> Gateway
    Gateway --> US
    Gateway --> OS
    Gateway --> PS
 
    style Clients fill:#374151,stroke:#a855f7,stroke-width:2px
    style Gateway fill:#7c3aed,stroke:#a855f7,stroke-width:2px
    style US fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style OS fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style PS fill:#6366f1,stroke:#a855f7,stroke-width:2px

Gateway responsibilities:

  • Routing: Direct requests to appropriate services
  • Authentication: Validate tokens, enforce security
  • Rate limiting: Protect services from overload
  • Load balancing: Distribute traffic across instances
  • Response aggregation: Combine responses from multiple services
  • Protocol translation: REST to gRPC, etc.

How do you configure Spring Cloud Gateway?

Spring Cloud Gateway is the current Spring gateway project. Its full server and lighter Proxy Exchange flavors support both WebFlux and Web MVC variants; choose the reactive or Servlet model that fits the application. Routes can be configured declaratively or programmatically.

The gateway integrates with service discovery (Eureka, Consul) for dynamic routing - the lb:// prefix enables load-balanced calls to registered services. Filters modify requests and responses, and you can add circuit breakers for resilience.

// Spring Cloud Gateway configuration
@Configuration
public class GatewayConfig {
 
    @Bean
    public RouteLocator customRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("users", r -> r
                .path("/api/users/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Request-Source", "gateway"))
                .uri("lb://user-service"))
            .route("orders", r -> r
                .path("/api/orders/**")
                .filters(f -> f
                    .stripPrefix(1)
                    .circuitBreaker(c -> c
                        .setName("ordersCircuitBreaker")
                        .setFallbackUri("forward:/fallback/orders")))
                .uri("lb://order-service"))
            .build();
    }
}

Service Discovery Questions

In dynamic environments, services come and go. Discovery solves "where is service X?"

What is service discovery and why do microservices need it?

Service discovery maps logical service names to current endpoints without hardcoded instance addresses. With a client-side registry such as Eureka, instances register and clients query the registry. In Kubernetes, the control plane publishes Service and EndpointSlice data and workloads normally resolve a stable Service name through DNS.

Without service discovery, you'd need to manually configure every service with the addresses of every other service it calls - and update all configurations whenever anything changes. Service discovery automates this, enabling dynamic scaling, rolling deployments, and failover.

flowchart TB
    subgraph Registry["Service Registry"]
        direction TB
        subgraph UserSvc["user-service"]
            U1["192.168.1.10:8080 healthy"]
            U2["192.168.1.11:8080 healthy"]
        end
        subgraph OrderSvc["order-service"]
            O1["192.168.1.20:8080 healthy"]
            O2["192.168.1.21:8080 unhealthy"]
        end
    end
 
    style Registry fill:#1e1b4b,stroke:#a855f7,stroke-width:2px
    style UserSvc fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style OrderSvc fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style U1 fill:#22c55e,stroke:#16a34a
    style U2 fill:#22c55e,stroke:#16a34a
    style O1 fill:#22c55e,stroke:#16a34a
    style O2 fill:#ef4444,stroke:#dc2626

How do you configure service registration with Eureka?

Netflix Eureka remains a supported service registry for Spring Cloud applications. Services register as Eureka clients and send heartbeats; the registry expires instances according to its lease and eviction configuration. In Kubernetes-native deployments, built-in Services and DNS often remove the need to run Eureka separately.

Add the Eureka client starter and configure the registry URL. @EnableDiscoveryClient is no longer required when a DiscoveryClient implementation is on the classpath, although leaving it explicit is supported. Eureka servers can be deployed as peers for high availability.

# application.yml
spring:
  application:
    name: order-service
 
eureka:
  client:
    service-url:
      defaultZone: http://eureka-server:8761/eureka/
  instance:
    prefer-ip-address: true
    lease-renewal-interval-in-seconds: 10
    lease-expiration-duration-in-seconds: 30
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

What is the difference between client-side and server-side service discovery?

In client-side discovery, the client queries the service registry, receives a list of available instances, and chooses one using a load balancing algorithm. The client is responsible for selecting the instance. In server-side discovery, the client calls a load balancer which queries the registry and routes the request.

Client-side gives more control over load balancing and can implement sticky sessions, but requires a discovery-aware client library. Server-side is simpler for clients and language-agnostic, but adds a network hop. In Kubernetes environments, server-side discovery through Services is the standard approach.

Client-side discovery:

flowchart TB
    Client["Client"]
    Registry["Registry"]
    I1["Instance 1"]
    I2["Instance 2"]
 
    Client -->|"1. Query registry"| Registry
    Client -->|"2. Choose instance"| I1
    Client -.->|"alternative"| I2
 
    style Client fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style Registry fill:#7c3aed,stroke:#a855f7,stroke-width:2px
    style I1 fill:#22c55e,stroke:#16a34a,stroke-width:2px
    style I2 fill:#22c55e,stroke:#16a34a,stroke-width:2px
// Spring Cloud LoadBalancer (client-side)
@Configuration
public class LoadBalancerConfig {
 
    @Bean
    @LoadBalanced  // Enables client-side load balancing
    public RestClient.Builder restClientBuilder() {
        return RestClient.builder();
    }
}
 
// Usage - "user-service" resolved via discovery
restClientBuilder.build().get()
    .uri("http://user-service/api/users/1")
    .retrieve()
    .body(UserDTO.class);

Server-side discovery (Kubernetes):

flowchart TB
    Client["Client"]
    LB["Load Balancer"]
    I1["Instance 1"]
    I2["Instance 2"]
 
    Client -->|"1. Call load balancer"| LB
    LB -->|"2. Routes to instance"| I1
    LB -.->|"or"| I2
 
    style Client fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style LB fill:#7c3aed,stroke:#a855f7,stroke-width:2px
    style I1 fill:#22c55e,stroke:#16a34a,stroke-width:2px
    style I2 fill:#22c55e,stroke:#16a34a,stroke-width:2px
# Kubernetes Service (server-side discovery)
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP

Circuit Breaker Questions

Circuit breakers prevent cascading failures in distributed systems.

What is the circuit breaker pattern and why is it important?

The circuit breaker pattern prevents an application from repeatedly calling a failing service. Like an electrical circuit breaker, it "trips" when failures exceed a threshold, stopping further calls until the service recovers. This prevents cascading failures where one slow service brings down the entire system.

Without circuit breakers, a failing downstream service causes requests to pile up, consuming threads and connections. Eventually the calling service also fails, and the failure cascades through the system. Circuit breakers fail fast, freeing resources and allowing graceful degradation.

stateDiagram-v2
    direction LR
    [*] --> CLOSED
    CLOSED --> OPEN: failures > threshold
    OPEN --> HALF_OPEN: timeout expires
    HALF_OPEN --> CLOSED: test succeeds
    HALF_OPEN --> OPEN: test fails
 
    state CLOSED {
        [*] --> Normal
        Normal: ✅ Requests pass through
    }
 
    state OPEN {
        [*] --> Failing
        Failing: 🚫 Requests fail fast
    }
 
    state HALF_OPEN {
        [*] --> Testing
        Testing: 🔄 Test requests allowed
    }

How do you implement a circuit breaker with Resilience4j?

Resilience4j is a current circuit-breaker option for Spring applications; Netflix Hystrix is no longer under active development. Resilience4j supports annotations and a programmatic API and can evaluate failure and slow-call rates over count- or time-based windows.

Configuration defines thresholds for failure rate, slow-call rate, window size, and timing. A fallback must be semantically safe: cached data should be marked stale, and fabricated business data such as a fake user is usually worse than an explicit unavailable response.

// Resilience4j Circuit Breaker
@Service
public class OrderService {
 
    private final CircuitBreaker circuitBreaker;
    private final UserClient userClient;
 
    public OrderService(CircuitBreakerRegistry registry, UserClient userClient) {
        this.circuitBreaker = registry.circuitBreaker("userService");
        this.userClient = userClient;
    }
 
    public UserDTO getUser(Long userId) {
        return circuitBreaker.executeSupplier(() -> userClient.getUser(userId));
    }
 
    // Or with annotations
    @CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
    public UserDTO getUserWithAnnotation(Long userId) {
        return userClient.getUser(userId);
    }
 
    private UserDTO getUserFallback(Long userId, Exception ex) {
        log.warn("Fallback for user {}: {}", userId, ex.getMessage());
        throw new ServiceUnavailableException("User service unavailable", ex);
    }
}
resilience4j:
  circuitbreaker:
    instances:
      userService:
        sliding-window-size: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
        permitted-number-of-calls-in-half-open-state: 3
        slow-call-rate-threshold: 80
        slow-call-duration-threshold: 2s

Resilience Pattern Questions

Beyond circuit breakers, several patterns help build resilient microservices.

What is the retry pattern and when should you use it?

The retry pattern automatically retries failed operations that might succeed on subsequent attempts. It's effective for transient failures like network glitches, temporary service unavailability, or database connection issues. The key is identifying which failures are retryable.

Configure retries with exponential backoff to avoid overwhelming a recovering service. Set maximum attempts to prevent infinite loops. Importantly, only retry idempotent operations - retrying a non-idempotent operation like "charge credit card" could result in duplicate charges.

@Retry(name = "userService", fallbackMethod = "getUserFallback")
public UserDTO getUser(Long userId) {
    return userClient.getUser(userId);
}
resilience4j:
  retry:
    instances:
      userService:
        max-attempts: 3
        wait-duration: 500ms
        exponential-backoff-multiplier: 2
        retry-exceptions:
          - java.io.IOException
          - java.net.SocketTimeoutException
        ignore-exceptions:
          - com.example.BusinessException

What is the bulkhead pattern?

The bulkhead pattern isolates failures by limiting concurrent calls to a service. Named after ship bulkheads that prevent a hull breach from sinking the entire ship, this pattern ensures that problems with one service don't exhaust resources needed for others.

Resilience4j offers two bulkhead types: semaphore (limits concurrent calls) and thread pool (isolates calls in separate threads). Thread pool bulkhead provides stronger isolation but has higher overhead. Use semaphore for most cases, thread pool when you need complete isolation.

@Bulkhead(name = "userService", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<UserDTO> getUser(Long userId) {
    return CompletableFuture.supplyAsync(() -> userClient.getUser(userId));
}
resilience4j:
  bulkhead:
    instances:
      userService:
        max-concurrent-calls: 20
        max-wait-duration: 500ms
  thread-pool-bulkhead:
    instances:
      userService:
        max-thread-pool-size: 10
        core-thread-pool-size: 5
        queue-capacity: 20

How do you combine multiple resilience patterns?

In production, you typically combine circuit breaker, retry, bulkhead, and timeout patterns. The order matters - patterns are applied from innermost to outermost. A typical combination: timeout wraps the actual call, bulkhead limits concurrency, retry handles transient failures, circuit breaker prevents repeated calls to a failing service.

The execution order when using annotations is: TimeLimiter → Bulkhead → Retry → CircuitBreaker. This means the circuit breaker sees retried attempts as separate calls, and the bulkhead limits how many retries can happen concurrently.

@CircuitBreaker(name = "userService", fallbackMethod = "fallback")
@Retry(name = "userService")
@Bulkhead(name = "userService")
@TimeLimiter(name = "userService")
public CompletableFuture<UserDTO> getUser(Long userId) {
    return CompletableFuture.supplyAsync(() -> userClient.getUser(userId));
}
 
// Execution order: TimeLimiter → Bulkhead → Retry → CircuitBreaker
resilience4j:
  timelimiter:
    instances:
      userService:
        timeout-duration: 2s
        cancel-running-future: true

Database Per Service Questions

Data management is the hardest problem in microservices.

Why should each microservice have its own database?

The database-per-service pattern gives each service exclusive ownership of its data. Other services use its API or published events rather than querying its tables. Ownership can mean separate database servers, databases, or private schemas, depending on isolation and operational needs.

A shared schema can create tight coupling: schema changes and performance incidents may affect multiple services and reduce deployment autonomy. Separate ownership is a strong default for independent services, but physical isolation is a trade-off rather than a universal requirement.

flowchart TB
    subgraph Services["Microservices"]
        US["Users Service"]
        OS["Orders Service"]
        PS["Products Service"]
    end
 
    US --> MySQL[("MySQL")]
    OS --> Postgres[("Postgres")]
    PS --> MongoDB[("MongoDB")]
 
    style Services fill:#1e1b4b,stroke:#a855f7,stroke-width:2px
    style US fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style OS fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style PS fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style MySQL fill:#0ea5e9,stroke:#0284c7,stroke-width:2px
    style Postgres fill:#3b82f6,stroke:#2563eb,stroke-width:2px
    style MongoDB fill:#22c55e,stroke:#16a34a,stroke-width:2px

Benefits:

  • Services are truly independent
  • Can choose best database for use case
  • Schema changes don't affect other services
  • Independent scaling

Challenges:

  • No joins across services
  • Data duplication
  • Consistency is hard

How do you handle queries that need data from multiple services?

When you need data from multiple services, you have several options. API Composition has a service (often the API Gateway) call multiple services and combine results. CQRS maintains denormalized read models optimized for specific queries. Event-driven synchronization keeps local copies of needed data.

The best choice depends on consistency requirements and query patterns. API Composition is simplest but adds latency. CQRS provides fast reads but requires eventual consistency. Local copies work well for slowly-changing reference data.


Saga Pattern Questions

Sagas manage distributed transactions across microservices.

What is the saga pattern and why is it needed?

The saga pattern coordinates a business transaction as a sequence of local transactions. A step may publish an event or report to an orchestrator. If a later step fails, compensating actions try to counter the business effects of completed steps; they are new transactions, can fail themselves, and do not literally restore all prior state.

Two-phase commit can provide stronger atomicity when every participant and the operational environment support it, but it adds coordination, availability, and coupling costs. Sagas trade that atomicity for explicit intermediate states, retries, idempotency, and compensation. Some operations should instead be kept inside one service boundary.

What is the difference between choreography and orchestration in sagas?

Choreography is decentralized - services react to events without a central coordinator. Each service knows what events to listen for and what events to publish. Orchestration uses a central saga coordinator that tells each service what to do and tracks the overall state.

Choreography is simpler for small sagas and promotes loose coupling, but becomes hard to understand as sagas grow. Orchestration provides clear visibility into saga state and is easier to test, but the coordinator can become a bottleneck.

Choreography (event-driven):

flowchart LR
    subgraph Saga["Event-Driven Saga"]
        OS["Order Service"]
        IS["Inventory Service"]
        PS["Payment Service"]
    end
 
    OS -->|"OrderCreated"| IS
    IS -->|"InventoryReserved"| PS
    PS -->|"PaymentProcessed"| OS
 
    style Saga fill:#1e1b4b,stroke:#a855f7,stroke-width:2px
    style OS fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style IS fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style PS fill:#6366f1,stroke:#a855f7,stroke-width:2px
// Order Service - starts saga
@Service
public class OrderService {
 
    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        Order order = orderRepository.save(new Order(request, OrderStatus.PENDING));
        eventPublisher.publish(new OrderCreatedEvent(order));
        return order;
    }
 
    @EventHandler
    public void on(PaymentProcessedEvent event) {
        Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
        order.setStatus(OrderStatus.CONFIRMED);
        orderRepository.save(order);
        eventPublisher.publish(new OrderConfirmedEvent(order));
    }
 
    @EventHandler
    public void on(PaymentFailedEvent event) {
        // Compensating action
        Order order = orderRepository.findById(event.getOrderId()).orElseThrow();
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
        eventPublisher.publish(new OrderCancelledEvent(order));
    }
}
 
// Inventory Service - reacts to events
@Service
public class InventoryService {
 
    @EventHandler
    public void on(OrderCreatedEvent event) {
        try {
            reserveInventory(event.getItems());
            eventPublisher.publish(new InventoryReservedEvent(event.getOrderId()));
        } catch (InsufficientInventoryException e) {
            eventPublisher.publish(new InventoryReservationFailedEvent(event.getOrderId()));
        }
    }
 
    @EventHandler
    public void on(OrderCancelledEvent event) {
        // Compensating action - release reserved inventory
        releaseInventory(event.getOrderId());
    }
}

Orchestration (central coordinator):

flowchart TB
    Orch["Saga Orchestrator"]
    OS["Order Service"]
    IS["Inventory Service"]
    PS["Payment Service"]
 
    Orch -->|"1. Create"| OS
    Orch -->|"2. Reserve"| IS
    Orch -->|"3. Charge"| PS
 
    style Orch fill:#7c3aed,stroke:#a855f7,stroke-width:2px
    style OS fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style IS fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style PS fill:#6366f1,stroke:#a855f7,stroke-width:2px
// Saga Orchestrator
@Service
public class CreateOrderSaga {
 
    public Order execute(CreateOrderRequest request) {
        SagaExecution saga = SagaExecution.start();
 
        try {
            // Step 1: Create order
            Order order = orderService.createOrder(request);
            saga.addCompensation(() -> orderService.cancelOrder(order.getId()));
 
            // Step 2: Reserve inventory
            inventoryService.reserve(order.getItems());
            saga.addCompensation(() -> inventoryService.release(order.getId()));
 
            // Step 3: Process payment
            paymentService.process(order.getId(), order.getTotal());
            saga.addCompensation(() -> paymentService.refund(order.getId()));
 
            // Step 4: Confirm order
            orderService.confirm(order.getId());
 
            return order;
 
        } catch (Exception e) {
            saga.compensate();  // Run compensations in reverse order
            throw new SagaFailedException(e);
        }
    }
}

When should you choose choreography over orchestration?

Choose choreography for short workflows where reactions to domain events are natural. There is no central coordinator, but the broker and participating services can still fail; the main design risk is that workflow state and dependencies become hard to see.

Choose orchestration for workflows that need explicit state, deadlines, and a visible decision point. Make the orchestrator durable and horizontally scalable so it does not become a bottleneck or availability risk.


CQRS Questions

CQRS separates read and write operations for better scalability.

What is CQRS and when should you use it?

CQRS (Command Query Responsibility Segregation) separates models for changing state from models for reading it. The split does not require separate databases, event sourcing, or asynchronous replication; those are optional architectural choices. When a distinct read store is projected asynchronously, clients must handle lag.

Use CQRS when read and write patterns differ significantly, when you need to scale reads independently, when complex queries would slow down the write database, or when using event sourcing. Don't use it for simple CRUD applications - the complexity isn't worth it.

flowchart TB
    subgraph CQRS["CQRS Architecture"]
        direction TB
        subgraph Write["Write Side"]
            CMD["Commands"]
            WM["Write Model"]
            PG[("PostgreSQL")]
            CMD --> WM --> PG
        end
        subgraph Read["Read Side"]
            QRY["Queries"]
            RM["Read Model"]
            ES[("Elasticsearch")]
            QRY --> RM --> ES
        end
        WM -->|"Events"| RM
    end
 
    style CQRS fill:#1e1b4b,stroke:#a855f7,stroke-width:2px
    style Write fill:#6366f1,stroke:#a855f7,stroke-width:2px
    style Read fill:#7c3aed,stroke:#a855f7,stroke-width:2px
    style PG fill:#3b82f6,stroke:#2563eb,stroke-width:2px
    style ES fill:#f59e0b,stroke:#d97706,stroke-width:2px

How do you handle eventual consistency in CQRS?

CQRS is eventually consistent only when the read model is updated asynchronously. In that design, show processing states, use optimistic UI carefully, poll or subscribe for completion, and carry a version or correlation ID when read-your-writes behavior matters.

Accept that users might briefly see stale data. For most applications, milliseconds to seconds of inconsistency is acceptable. If you need stronger consistency for specific operations, query the write model directly for those cases.


Spring Cloud Questions

Spring Cloud provides tools for common microservices patterns.

What is Spring Cloud Config and how does it work?

Spring Cloud Config provides centralized configuration management for distributed systems. A Config Server can serve configuration from Git or other backends. Clients load configuration at startup; runtime refresh requires the relevant refresh mechanism and must be designed carefully because not every bean or property is safely refreshable.

This solves the problem of managing configuration across many services and environments. Instead of configuring each service separately, you maintain configuration files in Git with all the benefits of version control, audit trails, and pull request workflows.

# Config Server application.yml
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/company/config-repo
          search-paths: '{application}'
# Client application.yml
spring:
  application:
    name: order-service
  config:
    import: configserver:http://config-server:8888

Configuration files in git repo:

config-repo/
├── application.yml           # Shared by all services
├── order-service.yml         # Order service specific
├── order-service-prod.yml    # Order service production
└── user-service.yml          # User service specific

How do you implement declarative REST clients with OpenFeign?

OpenFeign creates REST clients from annotated interfaces - you declare the API contract, and Feign generates the implementation. This reduces boilerplate and makes service calls look like local method calls. Combined with service discovery, you just use service names instead of URLs.

OpenFeign can be combined with Spring Cloud CircuitBreaker and Spring Cloud LoadBalancer when the relevant dependencies and properties are enabled. Fallback factories expose the cause, but degraded behavior must remain truthful and safe for the business operation.

@FeignClient(
    name = "user-service",
    fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient {
 
    @GetMapping("/api/users/{id}")
    UserDTO getUser(@PathVariable Long id);
 
    @GetMapping("/api/users")
    List<UserDTO> getUsers(@RequestParam List<Long> ids);
 
    @PostMapping("/api/users")
    UserDTO createUser(@RequestBody CreateUserRequest request);
}
 
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
 
    @Override
    public UserClient create(Throwable cause) {
        return new UserClient() {
            @Override
            public UserDTO getUser(Long id) {
                log.error("Fallback for getUser: {}", cause.getMessage());
                return new UserDTO(id, "Unknown", "fallback@example.com");
            }
 
            @Override
            public List<UserDTO> getUsers(List<Long> ids) {
                return Collections.emptyList();
            }
 
            @Override
            public UserDTO createUser(CreateUserRequest request) {
                throw new ServiceUnavailableException("User service unavailable");
            }
        };
    }
}

How does distributed tracing work with Micrometer and Zipkin?

Distributed tracing tracks requests as they flow through multiple services. Each request gets a unique trace ID that's propagated to all services it touches. Within a trace, each service operation creates a span with timing information. Visualizing traces shows the full request path and where time is spent.

Modern Spring Boot observability uses Micrometer Observation and Micrometer Tracing rather than Spring Cloud Sleuth. Add the appropriate bridge and exporter, then configure sampling and the backend. Instrumented HTTP clients and messaging libraries can propagate trace context; custom clients, executors, or message formats may need explicit instrumentation.

# application.yml
management:
  tracing:
    sampling:
      probability: 1.0  # Sample all requests (use lower in prod)
  zipkin:
    tracing:
      endpoint: http://zipkin:9411/api/v2/spans
// Trace context is automatically propagated
@RestController
public class OrderController {
 
    private static final Logger log = LoggerFactory.getLogger(OrderController.class);
 
    @GetMapping("/orders/{id}")
    public Order getOrder(@PathVariable Long id) {
        // traceId and spanId automatically included in logs
        log.info("Fetching order {}", id);
        return orderService.findById(id);
    }
}
 
// Log output includes trace context:
// 2026-01-07 10:30:00 [order-service,abc123,def456] INFO  OrderController - Fetching order 42
//                      ^service   ^traceId ^spanId

Deployment and Observability Questions

Operating microservices requires strong DevOps practices.

How do you containerize a Spring Boot microservice?

Containerization packages your application and runtime into an image. For Spring Boot, a Dockerfile can start from a pinned, trusted JRE base image, copy the JAR, and define the entrypoint. Smaller images can reduce transfer size and package surface, but Alpine's musl libc and minimal tooling can introduce compatibility and debugging trade-offs.

Spring Boot 2.3+ includes built-in support for building OCI images with ./mvnw spring-boot:build-image, which uses Cloud Native Buildpacks and doesn't require a Dockerfile. For more control, write your own Dockerfile.

# Dockerfile
# Pin an immutable digest in production.
FROM eclipse-temurin:21-jre-alpine
 
WORKDIR /app
 
COPY target/order-service-*.jar app.jar
 
EXPOSE 8080
 
ENTRYPOINT ["java", "-jar", "app.jar"]
# compose.yaml for local development
services:
  eureka:
    image: eureka-server:dev
    ports:
      - "8761:8761"
 
  config-server:
    image: config-server:dev
    ports:
      - "8888:8888"
    depends_on:
      - eureka
 
  user-service:
    image: user-service:dev
    depends_on:
      - eureka
      - config-server
    environment:
      - EUREKA_URI=http://eureka:8761/eureka
 
  order-service:
    image: order-service:dev
    depends_on:
      - eureka
      - config-server
      - user-service
    environment:
      - EUREKA_URI=http://eureka:8761/eureka

depends_on controls startup order in this simple local example; it does not by itself prove that a dependency is ready. Add health checks and dependency conditions where startup readiness matters. Use immutable image digests for deployments.

How do you deploy microservices to Kubernetes?

Kubernetes manages containerized applications at scale. Each service becomes a Deployment (defines how to run your containers) and a Service (provides network access). Kubernetes handles scaling, rolling updates, and self-healing.

Configure health checks (readiness and liveness probes) so Kubernetes knows when your service is ready for traffic and when it needs to be restarted. Set resource requests and limits to ensure fair resource sharing and prevent runaway services from affecting others.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: order-service:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: kubernetes
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 30
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 60
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "1Gi"
              cpu: "500m"

What observability stack do you need for microservices?

Microservices require comprehensive observability: metrics (Prometheus + Grafana), logs (ELK stack or Loki), and traces (Zipkin or Jaeger). Together, these let you understand system behavior, diagnose issues, and track requests across services.

Spring Boot Actuator exposes metrics and health endpoints. Configure structured JSON logging for easy aggregation. Custom health indicators let you include downstream service health in your status.

Metrics (Prometheus + Grafana):

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  metrics:
    tags:
      application: ${spring.application.name}

Logging (ELK Stack):

<!-- logback-spring.xml -->
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
        <includeMdcKeyName>traceId</includeMdcKeyName>
        <includeMdcKeyName>spanId</includeMdcKeyName>
    </encoder>
</appender>

Health Checks:

@Component
public class ExternalServiceHealthIndicator implements HealthIndicator {
 
    @Override
    public Health health() {
        if (externalServiceIsHealthy()) {
            return Health.up()
                .withDetail("externalService", "Available")
                .build();
        }
        return Health.down()
            .withDetail("externalService", "Unavailable")
            .build();
    }
}

Microservices Testing Questions

Testing distributed systems requires multiple strategies.

How do you handle API versioning in microservices?

API versioning ensures backward compatibility as services evolve. Common approaches include URL versioning (/api/v1/users), header versioning (Accept: application/vnd.api.v1+json), and query parameter versioning (?version=1).

URL versioning is visible and easy to understand, while media-type or header versioning can keep resource URLs stable. Publish a deprecation and sunset policy based on real consumer migration needs rather than an arbitrary N-1 rule. Consumer-driven contract tests can detect many breaking changes before deployment.

How do you debug a request that spans multiple services?

Debugging distributed requests requires correlation. Every request gets a unique trace ID that's passed through all service calls. Centralized logging aggregates logs from all services, searchable by trace ID. Distributed tracing tools (Zipkin, Jaeger) visualize the request path with timing.

Start debugging by finding the trace ID (usually in the error response or logs), then search centralized logs for that ID to see all related log entries. Use the tracing UI to see which service calls succeeded or failed and how long each took.

How do you test microservices effectively?

Testing microservices requires a layered approach. Unit tests verify business logic in isolation. Integration tests verify each service works with its database (use Testcontainers for real databases). Contract tests (Pact) verify API compatibility between services. End-to-end tests verify complete flows but should be used sparingly.

The testing pyramid still applies: many unit tests, fewer integration tests, even fewer end-to-end tests. Contract tests are especially important - they catch breaking API changes without requiring all services to be running together.


Official References

Frequently Asked Questions

What are microservices and when should you use them?

Microservices are independently deployable services organized around business capabilities. Each service owns its data and implementation, although ownership does not require a separate database server. Use them when independent scaling or deployment and team autonomy justify distributed-system costs. For a small team or simple domain, a modular monolith is often the safer starting point.

How do microservices communicate with each other?

Synchronous calls such as HTTP/REST or gRPC give an immediate response but create temporal coupling. Messaging through Kafka, RabbitMQ, or another broker can decouple producer and consumer lifecycles, but requires explicit delivery, ordering, idempotency, and consistency decisions. Choose per workflow rather than applying one transport everywhere.

What is the circuit breaker pattern?

A circuit breaker stops repeated calls to an unhealthy dependency. CLOSED passes calls while collecting outcomes, OPEN fails fast, and HALF-OPEN admits probes before closing or reopening. It complements timeouts, bounded concurrency, retries, and load shedding; it is useful only when thresholds and fallbacks match the operation.

How do you handle transactions across microservices?

Start from the consistency requirement. When one ACID transaction cannot or should not span participants, a saga coordinates local transactions through choreography or orchestration. Compensations are new business actions, not guaranteed technical rollbacks, so they must be idempotent, retryable, and observable. Some domains may still require a transactional protocol or a different service boundary.

What is service discovery and why is it needed?

Service discovery maps a logical service name to available endpoints. In client-side discovery, a client or library such as Spring Cloud LoadBalancer selects an instance from a registry such as Eureka or Consul. In platforms such as Kubernetes, Services and DNS provide a stable endpoint while the platform routes to ready backends.

What are the main challenges of microservices architecture?

The main costs are network latency and partial failure, cross-service consistency, many deployables, harder debugging and testing, and coordination around contracts and compatibility. Cross-service ACID protocols exist but add coupling and operational trade-offs. Add service boundaries only when their organizational or technical benefits outweigh these costs.

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides