Mikroserwisy w Java/Spring – Pytania rekrutacyjne [Przewodnik 2026]

Sławomir Plamowski 28 min czytania
interview java microservices spring-boot spring-cloud

Mikroserwisy w Java/Spring – Pytania rekrutacyjne [Przewodnik 2026]

Architektura mikroserwisowa to standard w enterprise Java. Rekruterzy oczekują znajomości nie tylko Spring Boot, ale całego ekosystemu Spring Cloud oraz wzorców rozproszonych systemów.

Ten przewodnik zawiera 60+ pytań rekrutacyjnych z odpowiedziami w formatach 30-sekundowym i 2-minutowym.


Spis treści

  1. Podstawy mikroserwisów
  2. Spring Boot dla mikroserwisów
  3. Service Discovery
  4. API Gateway
  5. Komunikacja między serwisami
  6. Resilience i Circuit Breaker
  7. Konfiguracja rozproszona
  8. Rozproszone transakcje i Saga
  9. Event-Driven Architecture
  10. Observability i monitoring
  11. Bezpieczeństwo mikroserwisów
  12. Deployment i skalowanie

Podstawy mikroserwisów

Czym są mikroserwisy i jak różnią się od monolitu?

Odpowiedź 30-sekundowa: Mikroserwisy to architektura dzieląca aplikację na małe, niezależne usługi. Każda ma własną bazę danych, można ją deployować osobno. Monolit to jedna aplikacja, mikroserwisy to wiele komunikujących się usług.

Odpowiedź 2-minutowa: Mikroserwisy vs monolit:

MONOLIT:
┌─────────────────────────────────┐
│  UI + Business Logic + Data    │
│  ┌─────┐ ┌─────┐ ┌─────┐      │
│  │ Mod │ │ Mod │ │ Mod │      │
│  └──┬──┘ └──┬──┘ └──┬──┘      │
│     └───────┴───────┘          │
│        Shared DB               │
└─────────────────────────────────┘

MIKROSERWISY:
┌──────┐   ┌──────┐   ┌──────┐
│Svc A │   │Svc B │   │Svc C │
│ API  │◄─►│ API  │◄─►│ API  │
│ DB_A │   │ DB_B │   │ DB_C │
└──────┘   └──────┘   └──────┘

Cechy mikroserwisów:

  • Single Responsibility - każdy serwis ma jedną odpowiedzialność
  • Independently deployable - niezależny deployment
  • Own data - każdy serwis ma własną bazę danych
  • Technology agnostic - różne technologie w różnych serwisach
  • Decentralized governance - zespoły zarządzają swoimi serwisami

Kiedy mikroserwisy:

  • Duży zespół (podział na mniejsze zespoły)
  • Różne wymagania skalowania dla modułów
  • Potrzeba niezależnych deploymentów
  • System ma rosnąć przez lata

Kiedy monolit:

  • Mały zespół (1-5 osób)
  • MVP, proof of concept
  • Prosta domena biznesowa
  • Brak doświadczenia z distributed systems

Jakie są główne wyzwania architektury mikroserwisowej?

Odpowiedź 30-sekundowa: Główne wyzwania to: rozproszone transakcje, eventual consistency, debugging distributed systems, latency sieci, service discovery oraz operacyjna złożoność zarządzania wieloma serwisami.

Odpowiedź 2-minutowa:

1. Rozproszone transakcje:

// Brak ACID między serwisami
// Zamówienie wymaga: Order Service + Payment Service + Inventory Service
// Każdy ma własną bazę - nie ma jednej transakcji

2. Consistency:

Strong Consistency (monolit) → Eventual Consistency (mikroserwisy)

User zmienia adres w User Service
→ Order Service widzi stary adres przez chwilę
→ Eventually wszystko jest zsynchronizowane

3. Network latency:

Monolit:     method call = nanoseconds
Mikroserwisy: HTTP call = milliseconds (1000x wolniej)

4. Debugging i tracing:

Request → Gateway → Service A → Service B → Service C
                                     ↓
                               Gdzie błąd?

5. Data consistency:

// Jak zrobić JOIN między bazami różnych serwisów?
SELECT o.*, u.name
FROM orders o            -- Order Service DB
JOIN users u ON ...      -- User Service DB  ← Niemożliwe!

6. Operational complexity:

Monolit: 1 aplikacja, 1 deployment, 1 monitoring
Mikroserwisy: 50 serwisów, 50 deploymentów, 50 logów

Rozwiązania:

  • Saga pattern dla transakcji
  • Event Sourcing dla consistency
  • Distributed tracing (Zipkin, Jaeger)
  • API Gateway dla routing
  • Service mesh dla komunikacji

Czym jest Bounded Context w DDD?

Odpowiedź 30-sekundowa: Bounded Context to granica, w której model domenowy ma jednoznaczne znaczenie. Każdy mikroserwis powinien odpowiadać jednemu Bounded Context. User w serwisie Auth to co innego niż User w serwisie Billing.

Odpowiedź 2-minutowa:

E-COMMERCE DOMAIN:

┌─────────────────────┐  ┌─────────────────────┐
│   SALES CONTEXT     │  │  SHIPPING CONTEXT   │
│                     │  │                     │
│  Customer:          │  │  Recipient:         │
│  - email            │  │  - address          │
│  - preferences      │  │  - phone            │
│  - cart             │  │  - delivery window  │
│                     │  │                     │
│  Product:           │  │  Package:           │
│  - price            │  │  - weight           │
│  - description      │  │  - dimensions       │
│  - availability     │  │  - tracking number  │
└─────────────────────┘  └─────────────────────┘
         ↑                         ↑
    RÓŻNE MODELE TEJ SAMEJ RZECZYWISTOŚCI

Zasady:

// Sales Context
public class Customer {
    private Email email;
    private ShoppingCart cart;
    private Preferences preferences;
}

// Shipping Context - TEN SAM user, INNY model
public class Recipient {
    private Address shippingAddress;
    private Phone contactPhone;
    private DeliveryWindow preferredWindow;
}

Context Map - relacje między kontekstami:

  • Shared Kernel - wspólny kod (unikać!)
  • Customer-Supplier - jeden dostarcza, drugi konsumuje
  • Conformist - downstream akceptuje model upstream
  • Anti-Corruption Layer - tłumaczenie między kontekstami
// Anti-Corruption Layer
@Service
public class CustomerTranslator {
    public Recipient toRecipient(Customer customer) {
        return new Recipient(
            customer.getShippingAddress(),
            customer.getPhone()
        );
    }
}

Spring Boot dla mikroserwisów

Jak skonfigurować Spring Boot dla mikroserwisów?

Odpowiedź 30-sekundowa: Spring Boot z spring-boot-starter-web, externalized configuration przez application.yml i profile, health endpoints przez Actuator, oraz dependency na Spring Cloud dla service discovery i config server.

Odpowiedź 2-minutowa:

Podstawowa struktura projektu:

<!-- pom.xml -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2023.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Konfiguracja:

# application.yml
spring:
  application:
    name: order-service
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}

server:
  port: ${SERVER_PORT:8080}

eureka:
  client:
    service-url:
      defaultZone: ${EUREKA_URI:http://localhost:8761/eureka}
  instance:
    prefer-ip-address: true

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always

12-Factor App principles:

@Configuration
public class AppConfig {

    @Value("${database.url}")  // Z ENV lub Config Server
    private String dbUrl;

    @Value("${feature.new-checkout:false}")
    private boolean newCheckoutEnabled;
}

Co to jest Spring Cloud i jakie komponenty zawiera?

Odpowiedź 30-sekundowa: Spring Cloud to toolkit do budowy mikroserwisów. Główne komponenty: Eureka (service discovery), Spring Cloud Gateway (API gateway), Config Server (centralna konfiguracja), Resilience4j (circuit breaker), Sleuth/Micrometer (distributed tracing).

Odpowiedź 2-minutowa:

┌─────────────────────────────────────────────────────┐
│                   SPRING CLOUD                       │
├─────────────────────────────────────────────────────┤
│                                                      │
│  ┌──────────────┐    ┌──────────────┐              │
│  │ Config       │    │ Eureka       │              │
│  │ Server       │    │ (Discovery)  │              │
│  └──────────────┘    └──────────────┘              │
│                                                      │
│  ┌──────────────┐    ┌──────────────┐              │
│  │ API Gateway  │    │ LoadBalancer │              │
│  │              │    │              │              │
│  └──────────────┘    └──────────────┘              │
│                                                      │
│  ┌──────────────┐    ┌──────────────┐              │
│  │ Resilience4j │    │ Micrometer   │              │
│  │ (Circuit     │    │ Tracing      │              │
│  │  Breaker)    │    │              │              │
│  └──────────────┘    └──────────────┘              │
│                                                      │
└─────────────────────────────────────────────────────┘

Komponenty:

Komponent Zastosowanie
Eureka Service discovery - rejestracja i znajdowanie serwisów
Config Server Centralna konfiguracja dla wszystkich serwisów
Gateway API Gateway - routing, rate limiting, security
LoadBalancer Client-side load balancing
Resilience4j Circuit breaker, retry, rate limiter
Micrometer Tracing Distributed tracing (dawniej Sleuth)
OpenFeign Declarative REST client
Stream Event-driven messaging (Kafka/RabbitMQ)
// Przykład użycia Feign Client
@FeignClient(name = "user-service")
public interface UserClient {

    @GetMapping("/users/{id}")
    UserDto getUser(@PathVariable Long id);
}

Service Discovery

Jak działa Eureka Server i Client?

Odpowiedź 30-sekundowa: Eureka Server to rejestr serwisów. Eureka Client rejestruje się przy starcie i wysyła heartbeaty. Gdy serwis potrzebuje innego, pyta Eureka o jego adres. Eliminuje hardcoded URLs.

Odpowiedź 2-minutowa:

┌─────────────────────────────────────────────────────┐
│                  EUREKA SERVER                       │
│  ┌─────────────────────────────────────────────┐   │
│  │ Registry:                                    │   │
│  │  order-service  → [192.168.1.10:8080]       │   │
│  │                 → [192.168.1.11:8080]       │   │
│  │  user-service   → [192.168.1.20:8081]       │   │
│  │  payment-service→ [192.168.1.30:8082]       │   │
│  └─────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘
         ↑ register       ↑ heartbeat      ↓ fetch
    ┌─────────┐      ┌─────────┐      ┌─────────┐
    │ Order   │      │ User    │      │ Payment │
    │ Service │ ───► │ Service │ ◄─── │ Service │
    └─────────┘      └─────────┘      └─────────┘

Eureka Server:

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
# application.yml
server:
  port: 8761

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false

Eureka Client:

@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    lease-renewal-interval-in-seconds: 30
    lease-expiration-duration-in-seconds: 90

Używanie discovery:

@Service
public class OrderService {

    @Autowired
    private DiscoveryClient discoveryClient;

    public String getUserServiceUrl() {
        List<ServiceInstance> instances =
            discoveryClient.getInstances("user-service");
        return instances.get(0).getUri().toString();
    }
}

// Lub z LoadBalancer (preferowane)
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate();
}

// Użycie - nazwa serwisu zamiast IP
restTemplate.getForObject(
    "http://user-service/users/1",  // Eureka resolve
    User.class
);

Czym różni się client-side od server-side load balancing?

Odpowiedź 30-sekundowa: Server-side: centralny load balancer (nginx) decyduje gdzie wysłać request. Client-side: klient zna wszystkie instancje serwisu i sam wybiera (Spring Cloud LoadBalancer). Client-side jest typowy dla mikroserwisów.

Odpowiedź 2-minutowa:

SERVER-SIDE LOAD BALANCING:
┌────────┐      ┌──────────┐      ┌──────────┐
│ Client │ ──► │   Load   │ ──► │ Server 1 │
│        │      │ Balancer │ ──► │ Server 2 │
│        │      │ (nginx)  │ ──► │ Server 3 │
└────────┘      └──────────┘      └──────────┘
                     ↑
            Single point of failure

CLIENT-SIDE LOAD BALANCING:
┌────────────────────┐      ┌──────────┐
│ Client             │ ──► │ Server 1 │
│ ┌────────────────┐ │ ──► │ Server 2 │
│ │ LoadBalancer   │ │ ──► │ Server 3 │
│ │ [1,2,3] cached │ │      └──────────┘
│ └────────────────┘ │
└────────────────────┘
      ↑
  Eureka fetch

Spring Cloud LoadBalancer:

@Configuration
public class LoadBalancerConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

@Service
public class OrderService {

    @Autowired
    private RestTemplate restTemplate;

    public User getUser(Long id) {
        // "user-service" resolved przez LoadBalancer
        return restTemplate.getForObject(
            "http://user-service/users/" + id,
            User.class
        );
    }
}

Strategie load balancing:

// Round Robin (domyślna)
public class RoundRobinLoadBalancer { }

// Random
public class RandomLoadBalancer { }

// Custom - np. prefer same zone
@Bean
public ServiceInstanceListSupplier
        discoveryClientServiceInstanceListSupplier(
            ConfigurableApplicationContext context) {
    return ServiceInstanceListSupplier.builder()
        .withDiscoveryClient()
        .withSameInstancePreference()  // Sticky session
        .build(context);
}

API Gateway

Jak działa Spring Cloud Gateway?

Odpowiedź 30-sekundowa: Spring Cloud Gateway to API Gateway na Netty (reactive). Obsługuje routing, filtering, rate limiting, security. Request przychodzi, pasuje do Route, przechodzi przez Filters, trafia do serwisu. Jeden entry point dla wszystkich mikroserwisów.

Odpowiedź 2-minutowa:

                    ┌─────────────────────────────────┐
                    │      SPRING CLOUD GATEWAY       │
                    │                                 │
Client ──────────► │  Route Predicate                │
   │                │  ├─ Path=/users/**             │
   │                │  ├─ Host=api.example.com       │
   │                │  └─ Header=X-Request-Id        │
   │                │                                 │
   │                │  Filters (Pre)                  │
   │                │  ├─ AddRequestHeader            │
   │                │  ├─ RateLimiter                 │
   │                │  └─ Authentication              │
   │                │           │                     │
   │                │           ▼                     │
   │                │  ┌─────────────────┐           │
   │                │  │ user-service    │           │
   │                │  │ order-service   │           │
   │                │  │ payment-service │           │
   │                │  └─────────────────┘           │
   │                │           │                     │
   │                │  Filters (Post)                 │
   │                │  ├─ AddResponseHeader           │
   │                │  └─ ModifyResponseBody          │
   │                │                                 │
◄──┴────────────────│◄────────────────────────────────┘

Konfiguracja:

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service  # lb = LoadBalanced
          predicates:
            - Path=/api/users/**
          filters:
            - StripPrefix=1
            - AddRequestHeader=X-Request-Source, gateway

        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
            - Method=GET,POST
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20

Custom filter:

@Component
public class LoggingFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange,
                              GatewayFilterChain chain) {

        log.info("Request: {} {}",
            exchange.getRequest().getMethod(),
            exchange.getRequest().getPath());

        return chain.filter(exchange)
            .then(Mono.fromRunnable(() -> {
                log.info("Response: {}",
                    exchange.getResponse().getStatusCode());
            }));
    }

    @Override
    public int getOrder() {
        return -1; // Execute first
    }
}

Jak zaimplementować rate limiting w API Gateway?

Odpowiedź 30-sekundowa: W Spring Cloud Gateway używamy RequestRateLimiter filter z Redis. Definiujemy replenishRate (requests/sec) i burstCapacity (max burst). Key resolver określa po czym limitować - IP, user ID, API key.

Odpowiedź 2-minutowa:

spring:
  cloud:
    gateway:
      routes:
        - id: rate-limited-route
          uri: lb://api-service
          predicates:
            - Path=/api/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10   # 10 req/sec
                redis-rate-limiter.burstCapacity: 20   # max burst
                redis-rate-limiter.requestedTokens: 1  # tokens per request
                key-resolver: "#{@userKeyResolver}"

Key Resolvers:

@Configuration
public class RateLimiterConfig {

    // Limit per IP
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest()
                .getRemoteAddress()
                .getAddress()
                .getHostAddress()
        );
    }

    // Limit per user
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest()
                .getHeaders()
                .getFirst("X-User-Id")
        );
    }

    // Limit per API key
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest()
                .getQueryParams()
                .getFirst("apiKey")
        );
    }
}

Token Bucket Algorithm:

Bucket capacity: 20 tokens
Refill rate: 10 tokens/second

Request 1: 20 tokens → 19 tokens (ALLOWED)
Request 2: 19 tokens → 18 tokens (ALLOWED)
...
Request 20: 1 token → 0 tokens (ALLOWED)
Request 21: 0 tokens (REJECTED - 429 Too Many Requests)

After 1 second: 0 + 10 = 10 tokens
Request 22: 10 tokens → 9 tokens (ALLOWED)

Custom Rate Limiter:

@Component
public class TieredRateLimiter implements RateLimiter<TieredConfig> {

    @Override
    public Mono<Response> isAllowed(String routeId, String id) {
        // Premium users: 100 req/sec
        // Free users: 10 req/sec
        UserTier tier = getUserTier(id);
        int limit = tier == PREMIUM ? 100 : 10;

        return checkRedisLimit(id, limit);
    }
}

Komunikacja między serwisami

REST vs Messaging - kiedy co stosować?

Odpowiedź 30-sekundowa: REST (synchroniczne) gdy potrzebujesz natychmiastowej odpowiedzi - pobieranie danych, walidacja. Messaging (asynchroniczne) gdy operacja może poczekać - wysyłka emaila, przetwarzanie zamówienia, aktualizacja cache. Messaging zwiększa resilience.

Odpowiedź 2-minutowa:

SYNCHRONOUS (REST/gRPC):
┌────────┐  request   ┌────────┐
│Service │ ─────────► │Service │
│   A    │ ◄───────── │   B    │
└────────┘  response  └────────┘
     ↓
   Czeka na odpowiedź

ASYNCHRONOUS (Messaging):
┌────────┐  publish   ┌─────────┐  consume  ┌────────┐
│Service │ ─────────► │ Message │ ────────► │Service │
│   A    │            │  Queue  │           │   B    │
└────────┘            └─────────┘           └────────┘
     ↓
   Nie czeka

Kiedy REST:

// Potrzebna natychmiastowa odpowiedź
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable Long id) {
    UserDto user = userClient.getUser(order.getUserId()); // SYNC
    return createOrderDto(order, user);
}

// Walidacja przed zapisem
@PostMapping("/orders")
public Order createOrder(@RequestBody OrderRequest req) {
    boolean hasCredit = paymentClient.checkCredit(req.getUserId()); // SYNC
    if (!hasCredit) throw new InsufficientFundsException();
    return orderRepository.save(order);
}

Kiedy Messaging:

// Notyfikacje - nie potrzeba odpowiedzi
orderCreatedPublisher.publish(new OrderCreatedEvent(order));
// → Email Service przetworzy asynchronicznie
// → Inventory Service zaktualizuje stock

// Operacje mogące poczekać
@RabbitListener(queues = "order-processing")
public void processOrder(OrderMessage message) {
    // Może trwać minuty - użytkownik nie czeka
}

// Decoupling
// Order Service nie wie o Email Service
// Publikuje event, konsumenci reagują

Hybrid approach:

@Service
public class OrderService {

    public Order createOrder(OrderRequest req) {
        // SYNC - potrzebna walidacja
        boolean valid = inventoryClient.checkAvailability(req.getItems());
        if (!valid) throw new OutOfStockException();

        Order order = orderRepository.save(new Order(req));

        // ASYNC - notifications, analytics, etc.
        eventPublisher.publish(new OrderCreatedEvent(order));

        return order;
    }
}

Jak działa OpenFeign?

Odpowiedź 30-sekundowa: OpenFeign to deklaratywny REST client. Definiujesz interfejs z adnotacjami @FeignClient, @GetMapping, a Spring generuje implementację. Automatycznie integruje się z Eureka i LoadBalancer. Czystszy kod niż RestTemplate.

Odpowiedź 2-minutowa:

// Definicja Feign Client
@FeignClient(
    name = "user-service",  // Nazwa serwisu w Eureka
    fallback = UserClientFallback.class
)
public interface UserClient {

    @GetMapping("/users/{id}")
    UserDto getUser(@PathVariable Long id);

    @GetMapping("/users")
    List<UserDto> getUsers(@RequestParam String role);

    @PostMapping("/users")
    UserDto createUser(@RequestBody CreateUserRequest request);

    @PutMapping("/users/{id}")
    UserDto updateUser(@PathVariable Long id,
                       @RequestBody UpdateUserRequest request);
}

// Fallback dla Circuit Breaker
@Component
public class UserClientFallback implements UserClient {

    @Override
    public UserDto getUser(Long id) {
        return new UserDto(id, "Unknown", "user@default.com");
    }

    @Override
    public List<UserDto> getUsers(String role) {
        return Collections.emptyList();
    }

    // ...
}

Użycie:

@Service
@RequiredArgsConstructor
public class OrderService {

    private final UserClient userClient;  // Inject jak zwykły bean

    public OrderDto getOrderWithUser(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        UserDto user = userClient.getUser(order.getUserId());
        return new OrderDto(order, user);
    }
}

Konfiguracja:

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            connectTimeout: 5000
            readTimeout: 5000
            loggerLevel: basic
          user-service:  # Per-client config
            connectTimeout: 2000
            readTimeout: 10000

Custom configuration:

@Configuration
public class FeignConfig {

    @Bean
    public RequestInterceptor authInterceptor() {
        return requestTemplate -> {
            String token = SecurityContextHolder.getContext()
                .getAuthentication().getCredentials().toString();
            requestTemplate.header("Authorization", "Bearer " + token);
        };
    }

    @Bean
    public ErrorDecoder errorDecoder() {
        return (methodKey, response) -> {
            if (response.status() == 404) {
                return new ResourceNotFoundException();
            }
            return new FeignException.errorStatus(methodKey, response);
        };
    }
}

Resilience i Circuit Breaker

Co to jest Circuit Breaker i jak działa?

Odpowiedź 30-sekundowa: Circuit Breaker chroni przed cascading failures. Trzy stany: CLOSED (normalne działanie), OPEN (blokuje requesty po przekroczeniu progu błędów), HALF-OPEN (testuje czy serwis wrócił). Resilience4j to standardowa implementacja w Spring.

Odpowiedź 2-minutowa:

┌─────────────────────────────────────────────────────┐
│              CIRCUIT BREAKER STATES                  │
│                                                      │
│     ┌──────────┐                                    │
│     │  CLOSED  │ ◄───── Normal operation            │
│     └────┬─────┘        Calls pass through          │
│          │                                          │
│          │ failure threshold exceeded               │
│          ▼                                          │
│     ┌──────────┐                                    │
│     │   OPEN   │ ◄───── Fast fail                   │
│     └────┬─────┘        Returns fallback            │
│          │                                          │
│          │ wait duration elapsed                    │
│          ▼                                          │
│     ┌───────────┐                                   │
│     │ HALF-OPEN │ ◄───── Test state                 │
│     └─────┬─────┘        Permits few calls          │
│           │                                         │
│     success │ failure                               │
│           ▼     ▼                                   │
│       CLOSED   OPEN                                 │
│                                                      │
└─────────────────────────────────────────────────────┘

Resilience4j configuration:

resilience4j:
  circuitbreaker:
    instances:
      userService:
        registerHealthIndicator: true
        slidingWindowSize: 10
        minimumNumberOfCalls: 5
        permittedNumberOfCallsInHalfOpenState: 3
        waitDurationInOpenState: 10s
        failureRateThreshold: 50
        slowCallRateThreshold: 100
        slowCallDurationThreshold: 2s

Użycie z adnotacjami:

@Service
public class OrderService {

    @CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
    public UserDto getUser(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", null);
    }
}

Łączenie z innymi patterns:

@CircuitBreaker(name = "backend")
@RateLimiter(name = "backend")
@Retry(name = "backend")
@TimeLimiter(name = "backend")
public CompletableFuture<String> doSomething() {
    return CompletableFuture.supplyAsync(() ->
        backendService.doSomething()
    );
}

Kolejność wykonania:

Retry → CircuitBreaker → RateLimiter → TimeLimiter → Bulkhead → Actual call

Jak działa Bulkhead pattern?

Odpowiedź 30-sekundowa: Bulkhead izoluje zasoby między komponentami. Jak przegrody w statku - awaria jednej sekcji nie zatopi całego statku. W mikroserwisach: osobne thread pools dla różnych serwisów. Jeden wolny serwis nie zablokuje wszystkich wątków.

Odpowiedź 2-minutowa:

BEZ BULKHEAD:
┌─────────────────────────────────────────┐
│          Shared Thread Pool (10)         │
│  ┌───┐┌───┐┌───┐┌───┐┌───┐            │
│  │ A ││ A ││ B ││ B ││ C │            │
│  └───┘└───┘└───┘└───┘└───┘            │
└─────────────────────────────────────────┘
         ↓
Jeśli Service A jest wolny → blokuje wszystkie wątki
→ Service B i C też nie mogą działać

Z BULKHEAD:
┌─────────────────┐ ┌─────────────────┐
│ Pool A (4)      │ │ Pool B (4)      │
│ ┌───┐┌───┐     │ │ ┌───┐┌───┐     │
│ │ A ││ A │     │ │ │ B ││ B │     │
│ └───┘└───┘     │ │ └───┘└───┘     │
└─────────────────┘ └─────────────────┘
         ↓
Service A wolny → tylko Pool A zablokowany
→ Service B nadal działa normalnie

Resilience4j Bulkhead:

resilience4j:
  bulkhead:
    instances:
      userService:
        maxConcurrentCalls: 10
        maxWaitDuration: 100ms
      paymentService:
        maxConcurrentCalls: 5
        maxWaitDuration: 500ms

  thread-pool-bulkhead:
    instances:
      userService:
        maxThreadPoolSize: 10
        coreThreadPoolSize: 5
        queueCapacity: 20

Semaphore vs ThreadPool Bulkhead:

// Semaphore Bulkhead - ogranicza concurrent calls
@Bulkhead(name = "userService", type = Type.SEMAPHORE)
public UserDto getUser(Long id) {
    return userClient.getUser(id);
}

// ThreadPool Bulkhead - osobny thread pool
@Bulkhead(name = "userService", type = Type.THREADPOOL)
public CompletableFuture<UserDto> getUserAsync(Long id) {
    return CompletableFuture.supplyAsync(() ->
        userClient.getUser(id)
    );
}

Real-world scenario:

@Service
public class OrderService {

    // Critical - payment must work
    @Bulkhead(name = "payment", fallbackMethod = "paymentFallback")
    public PaymentResult processPayment(Order order) {
        return paymentClient.charge(order);
    }

    // Non-critical - recommendations can fail
    @Bulkhead(name = "recommendations")
    public List<Product> getRecommendations(Long userId) {
        return recommendationClient.getForUser(userId);
    }
}

Konfiguracja rozproszona

Jak działa Spring Cloud Config Server?

Odpowiedź 30-sekundowa: Config Server centralizuje konfigurację wszystkich mikroserwisów. Przechowuje config w Git. Serwisy przy starcie pobierają swoją konfigurację. Wspiera profile (dev/prod), encryption i refresh bez restartu.

Odpowiedź 2-minutowa:

┌─────────────────────────────────────────────────────┐
│                    GIT REPOSITORY                    │
│  ┌────────────────────────────────────────────────┐ │
│  │ config-repo/                                   │ │
│  │   ├── application.yml      (shared)           │ │
│  │   ├── order-service.yml                       │ │
│  │   ├── order-service-dev.yml                   │ │
│  │   ├── order-service-prod.yml                  │ │
│  │   ├── user-service.yml                        │ │
│  │   └── payment-service.yml                     │ │
│  └────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬┘
                         │
              ┌──────────┴──────────┐
              │   CONFIG SERVER     │
              │   localhost:8888    │
              └──────────┬──────────┘
                    │    │    │
         ┌──────────┘    │    └──────────┐
         ▼               ▼               ▼
    ┌─────────┐    ┌─────────┐    ┌─────────┐
    │  Order  │    │  User   │    │ Payment │
    │ Service │    │ Service │    │ Service │
    └─────────┘    └─────────┘    └─────────┘

Config Server:

@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}
# Config Server application.yml
server:
  port: 8888

spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/company/config-repo
          default-label: main
          search-paths: '{application}'
        encrypt:
          enabled: true

Config Client:

# bootstrap.yml (lub spring.config.import w newer versions)
spring:
  application:
    name: order-service
  config:
    import: optional:configserver:http://localhost:8888
  profiles:
    active: dev

Encryption:

# Encrypt sensitive data
curl -X POST http://localhost:8888/encrypt -d "db-password"
# Returns: AQB8...encrypted...

# W config file:
spring:
  datasource:
    password: '{cipher}AQB8...encrypted...'

Dynamic refresh:

@RefreshScope
@RestController
public class ConfigController {

    @Value("${feature.enabled}")
    private boolean featureEnabled;

    // POST /actuator/refresh → reloads @Value fields
}

Rozproszone transakcje i Saga

Jak działa wzorzec Saga?

Odpowiedź 30-sekundowa: Saga to sekwencja lokalnych transakcji. Każda publikuje event uruchamiający kolejną. Przy błędzie wykonywane są transakcje kompensacyjne (odwrócenie). Dwa warianty: choreography (eventy) i orchestration (centralny koordynator).

Odpowiedź 2-minutowa:

ORDER SAGA - Happy Path:
┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐
│ Order   │───►│Inventory│───►│ Payment │───►│Shipping │
│ Created │    │Reserved │    │ Charged │    │ Started │
└─────────┘    └─────────┘    └─────────┘    └─────────┘
    T1             T2             T3             T4

ORDER SAGA - Failure at T3:
┌─────────┐    ┌─────────┐    ┌─────────┐
│ Order   │───►│Inventory│───►│ Payment │ ✗ FAILED
│ Created │    │Reserved │    │ Failed  │
└─────────┘    └─────────┘    └─────────┘
                    │              │
                    ▼              │
              ┌───────────┐        │
              │ Inventory │◄───────┘
              │ Released  │  Compensate
              │    C2     │
              └───────────┘
                    │
                    ▼
              ┌───────────┐
              │  Order    │
              │ Cancelled │
              │    C1     │
              └───────────┘

Choreography (event-driven):

// Order Service
@Service
public class OrderService {

    @Transactional
    public Order createOrder(OrderRequest req) {
        Order order = orderRepository.save(new Order(req));
        eventPublisher.publish(new OrderCreatedEvent(order));
        return order;
    }

    @EventListener
    public void onPaymentFailed(PaymentFailedEvent event) {
        Order order = orderRepository.findById(event.getOrderId());
        order.setStatus(CANCELLED);
        orderRepository.save(order);
    }
}

// Inventory Service
@Service
public class InventoryService {

    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        try {
            reserveInventory(event.getItems());
            eventPublisher.publish(new InventoryReservedEvent(event.getOrderId()));
        } catch (Exception e) {
            eventPublisher.publish(new InventoryReservationFailedEvent(event.getOrderId()));
        }
    }

    @EventListener
    public void onPaymentFailed(PaymentFailedEvent event) {
        releaseInventory(event.getOrderId());  // Compensate
    }
}

Orchestration (central coordinator):

@Service
public class OrderSagaOrchestrator {

    public void executeOrderSaga(OrderRequest req) {
        String sagaId = UUID.randomUUID().toString();

        try {
            // Step 1: Create Order
            Order order = orderService.createOrder(req);
            sagaLog.log(sagaId, "ORDER_CREATED", order.getId());

            // Step 2: Reserve Inventory
            inventoryClient.reserve(order.getItems());
            sagaLog.log(sagaId, "INVENTORY_RESERVED", order.getId());

            // Step 3: Charge Payment
            paymentClient.charge(order.getUserId(), order.getTotal());
            sagaLog.log(sagaId, "PAYMENT_CHARGED", order.getId());

            // Step 4: Start Shipping
            shippingClient.createShipment(order);
            sagaLog.log(sagaId, "SAGA_COMPLETED", order.getId());

        } catch (PaymentException e) {
            compensate(sagaId, "PAYMENT_FAILED");
        } catch (InventoryException e) {
            compensate(sagaId, "INVENTORY_FAILED");
        }
    }

    private void compensate(String sagaId, String failedStep) {
        List<SagaStep> completedSteps = sagaLog.getCompletedSteps(sagaId);

        // Execute compensations in reverse order
        for (SagaStep step : reversed(completedSteps)) {
            switch (step.getAction()) {
                case "INVENTORY_RESERVED" -> inventoryClient.release(step.getOrderId());
                case "ORDER_CREATED" -> orderService.cancel(step.getOrderId());
            }
        }
    }
}

Czym jest Event Sourcing?

Odpowiedź 30-sekundowa: Event Sourcing przechowuje zmiany stanu jako sekwencję eventów, nie aktualny stan. Account nie ma balance, ma listę: Deposited(100), Withdrawn(50). Stan odtwarzasz przez replay eventów. Pełna historia zmian, auditing, time travel.

Odpowiedź 2-minutowa:

TRADITIONAL (CRUD):
┌─────────────────────────────────┐
│ Account                         │
│ id: 123                         │
│ balance: 150     ← Current state│
│ updated_at: 2024-01-15          │
└─────────────────────────────────┘
     Skąd te 150? Nie wiadomo.

EVENT SOURCING:
┌─────────────────────────────────┐
│ Event Store                     │
│                                 │
│ 1. AccountCreated(id=123)       │
│ 2. MoneyDeposited(100)          │
│ 3. MoneyWithdrawn(30)           │
│ 4. MoneyDeposited(80)           │
│                                 │
│ Replay: 0 + 100 - 30 + 80 = 150│
└─────────────────────────────────┘
     Pełna historia!

Implementacja:

// Events
public sealed interface AccountEvent {
    record AccountCreated(String accountId, String owner) implements AccountEvent {}
    record MoneyDeposited(String accountId, BigDecimal amount) implements AccountEvent {}
    record MoneyWithdrawn(String accountId, BigDecimal amount) implements AccountEvent {}
}

// Aggregate
public class Account {
    private String id;
    private BigDecimal balance = BigDecimal.ZERO;
    private List<AccountEvent> changes = new ArrayList<>();

    // Rebuild from events
    public static Account fromEvents(List<AccountEvent> events) {
        Account account = new Account();
        events.forEach(account::apply);
        return account;
    }

    // Apply event (state change)
    private void apply(AccountEvent event) {
        switch (event) {
            case AccountCreated e -> this.id = e.accountId();
            case MoneyDeposited e -> this.balance = balance.add(e.amount());
            case MoneyWithdrawn e -> this.balance = balance.subtract(e.amount());
        }
    }

    // Command → Event
    public void deposit(BigDecimal amount) {
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        var event = new MoneyDeposited(this.id, amount);
        apply(event);
        changes.add(event);  // Uncommitted changes
    }

    public List<AccountEvent> getUncommittedChanges() {
        return new ArrayList<>(changes);
    }
}

// Event Store
@Repository
public class EventStore {

    public void save(String aggregateId, List<AccountEvent> events) {
        events.forEach(event ->
            jdbcTemplate.update(
                "INSERT INTO events (aggregate_id, event_type, payload, timestamp) VALUES (?, ?, ?, ?)",
                aggregateId,
                event.getClass().getSimpleName(),
                objectMapper.writeValueAsString(event),
                Instant.now()
            )
        );
    }

    public List<AccountEvent> getEvents(String aggregateId) {
        return jdbcTemplate.query(
            "SELECT * FROM events WHERE aggregate_id = ? ORDER BY timestamp",
            (rs, i) -> deserialize(rs.getString("event_type"), rs.getString("payload")),
            aggregateId
        );
    }
}

CQRS + Event Sourcing:

Command Side:                Read Side:
┌──────────────┐            ┌──────────────┐
│   Command    │            │    Query     │
│   Handler    │            │   Handler    │
└──────┬───────┘            └──────▲───────┘
       │                           │
       ▼                           │
┌──────────────┐  projection ┌─────┴────────┐
│ Event Store  │ ──────────► │  Read Model  │
│ (append-only)│             │  (optimized) │
└──────────────┘             └──────────────┘

Event-Driven Architecture

Jak zaimplementować event-driven architecture w Spring?

Odpowiedź 30-sekundowa: Spring Cloud Stream abstrahuje message brokery (Kafka, RabbitMQ). Definiujesz Supplier, Consumer, Function beans. Konfiguracja w YAML określa bindings do topics/queues. Alternatywa: Spring Kafka/AMQP bezpośrednio.

Odpowiedź 2-minutowa:

# Spring Cloud Stream configuration
spring:
  cloud:
    stream:
      bindings:
        orderCreated-out-0:
          destination: orders
          content-type: application/json
        orderCreated-in-0:
          destination: orders
          group: inventory-service
          content-type: application/json
      kafka:
        binder:
          brokers: localhost:9092

Producer:

@Configuration
public class OrderEventProducer {

    @Bean
    public Supplier<OrderCreatedEvent> orderCreated() {
        return () -> {
            // Called periodically or triggered
            return new OrderCreatedEvent(...);
        };
    }
}

// Or imperative style
@Service
public class OrderService {

    private final StreamBridge streamBridge;

    public Order createOrder(OrderRequest req) {
        Order order = orderRepository.save(new Order(req));

        streamBridge.send("orderCreated-out-0",
            new OrderCreatedEvent(order.getId(), order.getItems()));

        return order;
    }
}

Consumer:

@Configuration
public class InventoryEventConsumer {

    @Bean
    public Consumer<OrderCreatedEvent> orderCreated() {
        return event -> {
            log.info("Received order: {}", event.getOrderId());
            inventoryService.reserveItems(event.getItems());
        };
    }
}

// Function (processor)
@Bean
public Function<OrderCreatedEvent, InventoryReservedEvent> processOrder() {
    return event -> {
        inventoryService.reserveItems(event.getItems());
        return new InventoryReservedEvent(event.getOrderId());
    };
}

Direct Kafka (without Stream):

// Producer
@Service
public class OrderProducer {

    private final KafkaTemplate<String, OrderEvent> kafkaTemplate;

    public void send(OrderEvent event) {
        kafkaTemplate.send("orders", event.getOrderId(), event);
    }
}

// Consumer
@Service
public class OrderConsumer {

    @KafkaListener(topics = "orders", groupId = "inventory-group")
    public void consume(OrderEvent event) {
        processOrder(event);
    }
}

Transactional outbox pattern:

@Service
public class OrderService {

    @Transactional
    public Order createOrder(OrderRequest req) {
        Order order = orderRepository.save(new Order(req));

        // Save event to outbox table (same transaction)
        outboxRepository.save(new OutboxEvent(
            "OrderCreated",
            objectMapper.writeValueAsString(new OrderCreatedEvent(order))
        ));

        return order;
    }
}

// Separate process reads outbox and publishes to Kafka
@Scheduled(fixedDelay = 1000)
public void publishOutboxEvents() {
    List<OutboxEvent> events = outboxRepository.findUnpublished();
    events.forEach(event -> {
        kafkaTemplate.send("orders", event.getPayload());
        event.markAsPublished();
        outboxRepository.save(event);
    });
}

Observability i monitoring

Jak zaimplementować distributed tracing?

Odpowiedź 30-sekundowa: Distributed tracing śledzi request przez wszystkie serwisy. Każdy request ma trace ID propagowany w headerach. Micrometer Tracing (dawniej Sleuth) automatycznie dodaje trace ID. Zipkin lub Jaeger wizualizują trace. Kluczowe dla debugowania.

Odpowiedź 2-minutowa:

Request flow z tracing:
                        Trace ID: abc-123
┌─────────┐         ┌─────────┐         ┌─────────┐
│ Gateway │ ──────► │  Order  │ ──────► │  User   │
│ Span 1  │         │ Span 2  │         │ Span 3  │
└─────────┘         └─────────┘         └─────────┘
    │                   │                   │
    │    parent: null   │  parent: span1    │  parent: span2
    │    span: span1    │  span: span2      │  span: span3
    │                   │                   │
    └───────────────────┴───────────────────┘
                        │
                        ▼
                   ┌─────────┐
                   │ Zipkin  │
                   │ Server  │
                   └─────────┘

Dependencies:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
</dependency>

Configuration:

management:
  tracing:
    sampling:
      probability: 1.0  # 100% sampling (dev), use 0.1 for prod
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans

logging:
  pattern:
    level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

Logs z trace ID:

INFO [order-service,abc123,span1] Processing order 42
INFO [user-service,abc123,span2] Fetching user 7
INFO [order-service,abc123,span1] Order completed

Custom spans:

@Service
public class OrderService {

    private final Tracer tracer;

    public Order processOrder(OrderRequest req) {
        Span span = tracer.nextSpan().name("process-order").start();

        try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
            span.tag("order.type", req.getType());
            span.event("validation-started");

            validate(req);

            span.event("validation-completed");

            return createOrder(req);
        } finally {
            span.end();
        }
    }
}

Baggage (custom context propagation):

// Set baggage
BaggageField userId = BaggageField.create("user-id");
userId.updateValue(currentTraceContext, "user-123");

// Read baggage in another service
String userId = BaggageField.getByName("user-id").getValue();

Jak monitorować zdrowie mikroserwisów?

Odpowiedź 30-sekundowa: Spring Actuator exposeuje /health, /metrics, /info. Health check sprawdza DB, Redis, external services. Metrics eksportujemy do Prometheus, wizualizacja w Grafanie. Custom health indicators dla business logic.

Odpowiedź 2-minutowa:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always
      show-components: always
  health:
    circuitbreakers:
      enabled: true

Health response:

{
  "status": "UP",
  "components": {
    "db": { "status": "UP", "details": { "database": "PostgreSQL" } },
    "redis": { "status": "UP" },
    "userService": { "status": "UP" },
    "diskSpace": { "status": "UP", "details": { "free": "50GB" } }
  }
}

Custom Health Indicator:

@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {

    private final PaymentGatewayClient client;

    @Override
    public Health health() {
        try {
            boolean isUp = client.ping();
            if (isUp) {
                return Health.up()
                    .withDetail("latency", client.getLatency() + "ms")
                    .build();
            } else {
                return Health.down()
                    .withDetail("reason", "Gateway not responding")
                    .build();
            }
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}

Metrics z Micrometer:

@Service
public class OrderService {

    private final Counter orderCounter;
    private final Timer orderProcessingTimer;

    public OrderService(MeterRegistry registry) {
        this.orderCounter = registry.counter("orders.created");
        this.orderProcessingTimer = registry.timer("orders.processing.time");
    }

    public Order createOrder(OrderRequest req) {
        return orderProcessingTimer.record(() -> {
            Order order = processOrder(req);
            orderCounter.increment();
            return order;
        });
    }
}

Prometheus metrics:

# HELP orders_created_total Total orders created
# TYPE orders_created_total counter
orders_created_total 1523

# HELP orders_processing_time_seconds Order processing time
# TYPE orders_processing_time_seconds histogram
orders_processing_time_seconds_bucket{le="0.1"} 1200
orders_processing_time_seconds_bucket{le="0.5"} 1480
orders_processing_time_seconds_sum 245.5
orders_processing_time_seconds_count 1523

Bezpieczeństwo mikroserwisów

Jak zabezpieczyć komunikację między mikroserwisami?

Odpowiedź 30-sekundowa: mTLS (mutual TLS) - oba końce weryfikują certyfikaty. JWT token propagowany między serwisami. Service mesh (Istio) automatyzuje mTLS. API Gateway waliduje external requests. Internal network isolation.

Odpowiedź 2-minutowa:

EXTERNAL → INTERNAL SECURITY:

┌─────────────────────────────────────────────────────┐
│                    TRUST BOUNDARY                    │
│                                                      │
│  External     ┌─────────────┐      Internal         │
│  Request ────►│ API Gateway │─────► Services        │
│  + JWT        │ - Validate  │       (trusted)       │
│               │ - Rate limit│                       │
│               │ - AuthN     │                       │
│               └─────────────┘                       │
│                                                      │
└─────────────────────────────────────────────────────┘

INTERNAL SERVICE-TO-SERVICE:

Option 1: mTLS
┌─────────┐  mutual TLS  ┌─────────┐
│Service A│◄────────────►│Service B│
└─────────┘  certificates └─────────┘

Option 2: JWT Propagation
┌─────────┐  JWT header  ┌─────────┐
│Service A│─────────────►│Service B│
└─────────┘              └─────────┘

Option 3: Service Mesh
┌─────────────────────────────────────┐
│ Istio / Linkerd                     │
│  ┌───────┐         ┌───────┐       │
│  │Sidecar│◄──mTLS──►│Sidecar│       │
│  │ Proxy │         │ Proxy │       │
│  └───┬───┘         └───┬───┘       │
│      │                 │            │
│  ┌───┴───┐         ┌───┴───┐       │
│  │Svc A  │         │Svc B  │       │
│  └───────┘         └───────┘       │
└─────────────────────────────────────┘

JWT Propagation:

// Feign interceptor - forward JWT
@Component
public class JwtPropagatingInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes attrs =
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        if (attrs != null) {
            String jwt = attrs.getRequest().getHeader("Authorization");
            if (jwt != null) {
                template.header("Authorization", jwt);
            }
        }
    }
}

// Validate JWT in each service
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter()))
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health").permitAll()
                .anyRequest().authenticated()
            )
            .build();
    }
}

Service accounts:

// Internal service-to-service auth
@Service
public class ServiceAuthenticator {

    public String getServiceToken() {
        // Get token for this service (from secret, vault, etc.)
        return jwtEncoder.encode(JwtClaimsSet.builder()
            .issuer("order-service")
            .subject("order-service")
            .claim("scope", "internal")
            .expiresAt(Instant.now().plusSeconds(300))
            .build()
        ).getTokenValue();
    }
}

Deployment i skalowanie

Jak deployować mikroserwisy na Kubernetes?

Odpowiedź 30-sekundowa: Każdy mikroserwis jako Deployment z Service. ConfigMaps dla konfiguracji, Secrets dla credentials. Ingress dla external traffic. HPA (Horizontal Pod Autoscaler) dla auto-skalowania. Helm charts lub Kustomize do zarządzania.

Odpowiedź 2-minutowa:

# deployment.yaml
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: company/order-service:1.0.0
          ports:
            - containerPort: 8080
          env:
            - name: SPRING_PROFILES_ACTIVE
              value: "kubernetes"
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-secrets
                  key: password
          resources:
            requests:
              memory: "256Mi"
              cpu: "200m"
            limits:
              memory: "512Mi"
              cpu: "500m"
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: order-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: order-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Spring Cloud Kubernetes:

# application.yml
spring:
  cloud:
    kubernetes:
      config:
        enabled: true
        sources:
          - name: order-service-config
      discovery:
        enabled: true  # Use K8s service discovery instead of Eureka

Helm chart structure:

order-service/
├── Chart.yaml
├── values.yaml
├── templates/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   └── hpa.yaml

Jak skalować mikroserwisy?

Odpowiedź 30-sekundowa: Horizontal scaling - więcej instancji za load balancerem. Kubernetes HPA na podstawie CPU/memory/custom metrics. Vertical scaling - większe zasoby (rzadziej). Stateless design kluczowy - sesje w Redis, nie w pamięci.

Odpowiedź 2-minutowa:

SCALING APPROACHES:

Vertical (Scale Up):
┌─────────┐      ┌───────────────┐
│ 1 CPU   │  ──► │   4 CPU       │
│ 1GB RAM │      │   8GB RAM     │
└─────────┘      └───────────────┘
  Limit: Max hardware

Horizontal (Scale Out):
┌─────────┐      ┌───┐┌───┐┌───┐┌───┐
│   1     │  ──► │ 1 ││ 2 ││ 3 ││ 4 │
│ instance│      └───┘└───┘└───┘└───┘
└─────────┘            + Load Balancer
  Limit: Almost none

Stateless design:

// ❌ BAD - Stateful
@RestController
public class CartController {
    private Map<String, Cart> carts = new HashMap<>();  // In-memory!

    @PostMapping("/cart/{userId}/add")
    public void addToCart(@PathVariable String userId, @RequestBody Item item) {
        carts.computeIfAbsent(userId, k -> new Cart()).add(item);
    }
}

// ✅ GOOD - Stateless
@RestController
public class CartController {
    private final RedisTemplate<String, Cart> redis;

    @PostMapping("/cart/{userId}/add")
    public void addToCart(@PathVariable String userId, @RequestBody Item item) {
        Cart cart = redis.opsForValue().get("cart:" + userId);
        if (cart == null) cart = new Cart();
        cart.add(item);
        redis.opsForValue().set("cart:" + userId, cart);
    }
}

Auto-scaling strategies:

# CPU-based (most common)
metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

# Memory-based
metrics:
  - type: Resource
    resource:
      name: memory
      target:
        type: AverageValue
        averageValue: 500Mi

# Custom metrics (requests per second)
metrics:
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: 1000

# External metrics (queue depth)
metrics:
  - type: External
    external:
      metric:
        name: rabbitmq_queue_messages
        selector:
          matchLabels:
            queue: orders
      target:
        type: AverageValue
        averageValue: 30

Scaling considerations:

  • Database connections pooling (each instance = more connections)
  • Warmup time (new instances need time to start)
  • Graceful shutdown (drain connections before termination)
  • Session affinity vs stateless (prefer stateless)

Podsumowanie

Mikroserwisy w Java/Spring to rozległy temat łączący:

  1. Architekturę - DDD, Bounded Contexts, decomposition
  2. Spring Cloud - Eureka, Gateway, Config, LoadBalancer
  3. Resilience - Circuit Breaker, Bulkhead, Retry
  4. Komunikacja - REST, Messaging, Event-Driven
  5. Transakcje - Saga, Event Sourcing, CQRS
  6. Observability - Tracing, Metrics, Health
  7. Security - mTLS, JWT, Service Mesh
  8. Deployment - Kubernetes, Auto-scaling

Kluczowe na rozmowie:

  • Zrozumienie trade-offs (monolit vs mikroserwisy)
  • Praktyczna znajomość Spring Cloud
  • Wzorce distributed systems (Saga, Circuit Breaker)
  • Doświadczenie z Kubernetes

Zobacz też


Ostatnia aktualizacja: Styczeń 2026

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.