Mikroserwisy w Java/Spring – Pytania rekrutacyjne [Przewodnik 2026]
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
- Podstawy mikroserwisów
- Spring Boot dla mikroserwisów
- Service Discovery
- API Gateway
- Komunikacja między serwisami
- Resilience i Circuit Breaker
- Konfiguracja rozproszona
- Rozproszone transakcje i Saga
- Event-Driven Architecture
- Observability i monitoring
- Bezpieczeństwo mikroserwisów
- 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:
- Architekturę - DDD, Bounded Contexts, decomposition
- Spring Cloud - Eureka, Gateway, Config, LoadBalancer
- Resilience - Circuit Breaker, Bulkhead, Retry
- Komunikacja - REST, Messaging, Event-Driven
- Transakcje - Saga, Event Sourcing, CQRS
- Observability - Tracing, Metrics, Health
- Security - mTLS, JWT, Service Mesh
- 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ż
- Kompletny Przewodnik - Rozmowa Java Backend Developer - główny przewodnik dla Java developerów
- REST API – Pytania rekrutacyjne - projektowanie API
- Docker – Pytania rekrutacyjne - konteneryzacja
- Kubernetes – Pytania rekrutacyjne - orkiestracja
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.
