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.
Microservices Fundamentals
Before diving into patterns, understand when microservices make sense - and when they don't.
Monolith vs Microservices
Monolithic architecture:
┌─────────────────────────────────────────┐
│ Monolith │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Users │ │ Orders │ │Payments │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Shared Database │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
Microservices architecture:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Users │ │ Orders │ │ Payments │
│ Service │◄──►│ Service │◄──►│ Service │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│Users DB │ │Orders DB│ │Payments │
└─────────┘ └─────────┘ │ DB │
└─────────┘
Interview question: "When would you choose microservices over a monolith?"
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
The worst mistake is premature decomposition. Start with a well-structured monolith, extract services when pain points emerge.
Microservices Characteristics
| Characteristic | Description |
|---|---|
| Single responsibility | Each service does one thing well |
| Independent deployment | Deploy without coordinating with other services |
| Decentralized data | Each service owns its data store |
| Smart endpoints, dumb pipes | Business logic in services, not middleware |
| Design for failure | Assume network is unreliable |
| Evolutionary design | Services can be rewritten/replaced |
Bounded Contexts (Domain-Driven Design)
Services should align with bounded contexts - areas where a particular domain model applies:
┌─────────────────────────────────────────────────────────┐
│ E-Commerce Domain │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Catalog │ │ Orders │ │ Shipping │ │
│ │ Context │ │ Context │ │ Context │ │
│ │ │ │ │ │ │ │
│ │ - Product │ │ - Order │ │ - Shipment │ │
│ │ - Category │ │ - LineItem │ │ - Tracking │ │
│ │ - Price │ │ - Customer │ │ - Address │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ "Product" means different things in each context: │
│ - Catalog: description, images, specs │
│ - Orders: SKU, quantity, price at time of purchase │
│ - Shipping: weight, dimensions, fragility │
└─────────────────────────────────────────────────────────┘
Each bounded context becomes a candidate for a microservice. The same real-world concept (Product) has different representations in each context.
Service Communication
How services talk to each other is fundamental to microservices design.
Synchronous Communication
REST/HTTP - The default choice for simplicity:
// Using RestTemplate (legacy)
@Service
public class OrderService {
private final RestTemplate restTemplate;
public UserDTO getUser(Long userId) {
return restTemplate.getForObject(
"http://user-service/api/users/{id}",
UserDTO.class,
userId
);
}
}
// Using WebClient (reactive, preferred)
@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);
}gRPC - High-performance binary protocol for internal services:
// 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);
}
}When to use each:
| Protocol | Use When |
|---|---|
| REST | External APIs, simple CRUD, wide compatibility |
| gRPC | Internal services, high throughput, streaming |
| GraphQL | Client needs flexible queries, multiple frontends |
Asynchronous Communication
Decouple services with message queues:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Order │────►│ Message │────►│ Inventory │
│ Service │ │ Queue │ │ Service │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ Shipping │
│ Service │
└─────────────┘
// Publishing events (Spring Cloud Stream / Kafka)
@Service
public class OrderService {
private final StreamBridge streamBridge;
@Transactional
public Order createOrder(CreateOrderRequest request) {
Order order = orderRepository.save(new Order(request));
// Publish event after successful save
OrderCreatedEvent event = new OrderCreatedEvent(
order.getId(),
order.getUserId(),
order.getItems()
);
streamBridge.send("orders-out", event);
return order;
}
}
// 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());
}
}
}Synchronous vs Asynchronous:
| Aspect | Synchronous | Asynchronous |
|---|---|---|
| Coupling | Temporal coupling (both must be available) | Decoupled (producer doesn't wait) |
| Latency | Adds to request latency | Non-blocking |
| Consistency | Immediate | Eventual |
| Failure handling | Immediate feedback | Requires dead letter queues |
| Debugging | Easier to trace | Requires distributed tracing |
API Gateway
Single entry point for external clients:
┌──────────────┐
Clients ───────►│ API Gateway │
└──────┬───────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Users │ │ Orders │ │ Products │
│ Service │ │ Service │ │ Service │
└──────────┘ └──────────┘ └──────────┘
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.
// 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 & Load Balancing
In dynamic environments, services come and go. Discovery solves "where is service X?"
Service Registry Pattern
┌─────────────────────────────────────────────────┐
│ Service Registry │
│ (Eureka / Consul / etcd) │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ user-service: │ │
│ │ - 192.168.1.10:8080 (healthy) │ │
│ │ - 192.168.1.11:8080 (healthy) │ │
│ │ │ │
│ │ order-service: │ │
│ │ - 192.168.1.20:8080 (healthy) │ │
│ │ - 192.168.1.21:8080 (unhealthy) │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Service registration (Eureka client):
# 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);
}
}Client-Side vs Server-Side Discovery
Client-side discovery (Netflix Ribbon, Spring Cloud LoadBalancer):
┌────────────┐ ┌──────────────┐
│ Client │────►│ Registry │ 1. Query registry
└─────┬──────┘ └──────────────┘
│
│ 2. Choose instance & call directly
▼
┌──────────┐ ┌──────────┐
│Instance 1│ │Instance 2│
└──────────┘ └──────────┘
// Spring Cloud LoadBalancer
@Configuration
public class LoadBalancerConfig {
@Bean
@LoadBalanced // Enables client-side load balancing
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
// Usage - "user-service" resolved via discovery
restTemplate.getForObject("http://user-service/api/users/1", UserDTO.class);Server-side discovery (Kubernetes, AWS ALB):
┌────────────┐ ┌──────────────┐
│ Client │────►│Load Balancer │ 1. Call load balancer
└────────────┘ └──────┬───────┘
│ 2. Routes to instance
▼
┌──────────┐ ┌──────────┐
│Instance 1│ │Instance 2│
└──────────┘ └──────────┘
Kubernetes does this automatically with Services:
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- port: 80
targetPort: 8080
type: ClusterIPInterview question: "What's the advantage of client-side over server-side discovery?"
Client-side: More control over load balancing algorithms, can implement sticky sessions, fewer network hops.
Server-side: Simpler client code, language-agnostic, centralized control.
In Kubernetes environments, server-side discovery is standard. In non-K8s Spring Cloud environments, client-side is common.
Resilience Patterns
Distributed systems fail in partial, unexpected ways. Resilience patterns prevent cascading failures.
Circuit Breaker
Prevents repeated calls to a failing service:
┌─────────────────────────────────────────────┐
│ │
▼ │
┌────────┐ failures ┌────────┐ │
│ CLOSED │───────────────►│ OPEN │ │
└────────┘ > threshold └───┬────┘ │
▲ │ │
│ │ timeout │
│ success ▼ │
│ ┌───────────┐ │
└────────────────────│ HALF-OPEN │───────────┘
test succeeds └───────────┘ test fails
// 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());
return new UserDTO(userId, "Unknown", "unknown@example.com");
}
}Configuration:
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: 2sRetry Pattern
Retry transient failures:
@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.BusinessExceptionBulkhead Pattern
Isolate failures by limiting concurrent calls:
@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: 20Timeout
Don't wait forever:
@TimeLimiter(name = "userService")
public CompletableFuture<UserDTO> getUser(Long userId) {
return CompletableFuture.supplyAsync(() -> userClient.getUser(userId));
}resilience4j:
timelimiter:
instances:
userService:
timeout-duration: 2s
cancel-running-future: trueCombining Patterns
Order matters! Typical combination:
@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 → CircuitBreakerData Management
Data in microservices is the hardest problem. No silver bullets here.
Database Per Service
Each service owns its data:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Users │ │ Orders │ │ Products │
│ Service │ │ Service │ │ Service │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
┌───┴───┐ ┌───┴───┐ ┌───┴───┐
│ MySQL │ │Postgres│ │MongoDB│
└───────┘ └───────┘ └───────┘
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
Saga Pattern
Manage transactions across services without distributed transactions:
Choreography (event-driven):
┌─────────┐ OrderCreated ┌───────────┐
│ Order │───────────────────►│ Inventory │
│ Service │ │ Service │
└─────────┘ └─────┬─────┘
▲ │
│ InventoryReserved
│ │
│ PaymentProcessed ▼
└──────────────────────┌───────────┐
│ Payment │
│ Service │
└───────────┘
// 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):
┌──────────────┐
│ Saga │
│ Orchestrator │
└──────┬───────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Order │ │ Inventory│ │ Payment │
│ Service │ │ Service │ │ Service │
└──────────┘ └──────────┘ └──────────┘
// 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);
}
}
}Interview question: "When would you choose choreography over orchestration?"
Choreography:
- Simple sagas with few steps
- Services need to be highly decoupled
- No single point of failure
- Risk: Hard to track saga state, complex debugging
Orchestration:
- Complex sagas with many steps
- Need clear visibility of saga state
- Easier testing and debugging
- Risk: Orchestrator becomes a bottleneck/SPOF
Eventual Consistency
Accept that data won't always be immediately consistent:
// Order Service might show order as "confirmed"
// Inventory Service might still show stock as "reserved" (not yet deducted)
// This inconsistency window is usually milliseconds to seconds
// Design for it:
// 1. Use status fields that reflect processing state
// 2. Show "processing" states to users
// 3. Implement idempotent operations (safe to retry)
// 4. Use correlation IDs for trackingCQRS (Command Query Responsibility Segregation)
Separate read and write models:
┌─────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Commands │ │ Queries │ │
│ │ (Write API) │ │ (Read API) │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ Events ┌─────────────────────┐ │
│ │ Write Model │─────────────►│ Read Model │ │
│ │ (Normalized)│ │ (Denormalized/Fast) │ │
│ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │
│ ┌────┴────┐ ┌──────┴──────┐ │
│ │PostgreSQL│ │Elasticsearch │ │
│ └─────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
Use CQRS when:
- Read and write patterns differ significantly
- Need to scale reads independently
- Complex queries would slow down the write database
- Event sourcing is used
Spring Cloud Ecosystem
Spring Cloud provides tools for common microservices patterns.
Config Server
Centralized configuration management:
# 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:8888Configuration 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
Spring Cloud Gateway
Modern API Gateway (replaced Zuul):
@SpringBootApplication
public class GatewayApplication {
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r
.path("/users/**")
.filters(f -> f
.rewritePath("/users/(?<segment>.*)", "/api/users/${segment}")
.addRequestHeader("X-Gateway", "spring-cloud"))
.uri("lb://user-service"))
.route("order-service", r -> r
.path("/orders/**")
.filters(f -> f
.rewritePath("/orders/(?<segment>.*)", "/api/orders/${segment}")
.circuitBreaker(c -> c
.setName("ordersCB")
.setFallbackUri("forward:/fallback")))
.uri("lb://order-service"))
.build();
}
}OpenFeign
Declarative REST client:
@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");
}
};
}
}Distributed Tracing (Micrometer + Zipkin)
Track requests across services:
# 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 ^spanIdDeployment & Observability
Operating microservices requires strong observability.
Containerization
Package services as containers:
# Dockerfile
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/order-service-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]# docker-compose.yml for local development
version: '3.8'
services:
eureka:
image: eureka-server:latest
ports:
- "8761:8761"
config-server:
image: config-server:latest
ports:
- "8888:8888"
depends_on:
- eureka
user-service:
image: user-service:latest
depends_on:
- eureka
- config-server
environment:
- EUREKA_URI=http://eureka:8761/eureka
order-service:
image: order-service:latest
depends_on:
- eureka
- config-server
- user-service
environment:
- EUREKA_URI=http://eureka:8761/eurekaKubernetes Deployment
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"Observability Stack
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();
}
}Common Interview Questions
Q: How do you handle versioning in microservices?
URL versioning (/api/v1/users), header versioning (Accept: application/vnd.api.v1+json), or query parameter (?version=1). Support at least N-1 versions. Use consumer-driven contract testing to catch breaking changes.
Q: How do you debug a request that spans multiple services?
Distributed tracing (Zipkin, Jaeger) with correlation IDs. Every log includes traceId. Use centralized logging (ELK) to aggregate logs. Tracing tools show the full request path with timing.
Q: What's the difference between orchestration and choreography?
Orchestration: Central coordinator controls the flow. Easier to understand, single point of failure. Choreography: Services react to events independently. More decoupled, harder to track overall flow.
Q: How do you test microservices?
Unit tests for business logic. Integration tests per service (Testcontainers). Contract tests (Pact) for API compatibility. End-to-end tests sparingly (slow, brittle). Component tests with mocked dependencies.
Related Resources
- Spring Boot Interview Guide - Foundation for Spring Cloud
- Docker Interview Guide - Containerization fundamentals
- Kubernetes Interview Guide - Container orchestration
- System Design Interview Guide - Architectural patterns
- REST API Interview Guide - API design principles
