Microservices Architecture Interview Guide: From Monolith to Distributed Systems

·16 min read
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.


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

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

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:

ProtocolUse When
RESTExternal APIs, simple CRUD, wide compatibility
gRPCInternal services, high throughput, streaming
GraphQLClient 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:

AspectSynchronousAsynchronous
CouplingTemporal coupling (both must be available)Decoupled (producer doesn't wait)
LatencyAdds to request latencyNon-blocking
ConsistencyImmediateEventual
Failure handlingImmediate feedbackRequires dead letter queues
DebuggingEasier to traceRequires 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: ClusterIP

Interview 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: 2s

Retry 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.BusinessException

Bulkhead 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: 20

Timeout

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: true

Combining 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 → CircuitBreaker

Data 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 tracking

CQRS (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: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

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 ^spanId

Deployment & 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/eureka

Kubernetes 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

Ready to ace your interview?

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

View PDF Guides