Article
Kuma is an open-source, universal service mesh that runs on both Kubernetes and VMs. Built by Kong, Kuma provides a robust control plane that works with Envoy proxy to deliver security, reliability, and observability for microservices. For Java applications, Kuma offers seamless integration with minimal code changes while providing enterprise-grade service mesh capabilities.
In this guide, we'll explore how to integrate Java microservices with Kuma service mesh to achieve traffic management, security, observability, and resilience.
Why Kuma Service Mesh for Java?
- Universal Deployment: Works across Kubernetes, VMs, and containers
- Envoy-Powered: Leverages battle-tested Envoy proxy
- Multi-Zone Support: Native multi-zone and multi-cloud capabilities
- Zero-Trust Security: Built-in mTLS and service-to-service authentication
- Comprehensive Observability: Metrics, tracing, and logging out of the box
- Developer-Friendly: Easy to adopt with minimal application changes
Part 1: Kuma Architecture Overview
1.1 Kuma Components
Java Application → Kuma Data Plane (Envoy) → Kuma Control Plane → Management APIs ↓ ↓ ↓ ↓ Business Logic Traffic Routing Policies Kubernetes Spring Boot Security Configuration Docker Micronaut Observability Service Discovery VM Quarkus
1.2 Project Structure
java-kuma-mesh/ ├── user-service/ │ ├── src/main/java/com/example/user/ │ ├── Dockerfile │ └── kuma/ │ └── mesh.yaml ├── order-service/ │ ├── src/main/java/com/example/order/ │ └── Dockerfile ├── api-gateway/ │ ├── src/main/java/com/example/gateway/ │ └── Dockerfile ├── kuma-config/ │ ├── mesh.yaml │ ├── traffic-route.yaml │ └── circuit-breaker.yaml └── kubernetes/ ├── kuma-install.yaml └── services/ ├── user-service.yaml └── order-service.yaml
Part 2: Dependencies and Setup
2.1 Maven Dependencies
<!-- pom.xml --> <properties> <spring-boot.version>3.1.0</spring-boot.version> <micrometer.version>1.11.5</micrometer.version> <resilience4j.version>2.1.0</resilience4j.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Observability --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>${micrometer.version}</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId> <version>${micrometer.version}</version> </dependency> <!-- Resilience --> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> <version>${resilience4j.version}</version> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-circuitbreaker</artifactId> <version>${resilience4j.version}</version> </dependency> <!-- OpenTelemetry --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-api</artifactId> <version>1.31.0</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <version>${spring-boot.version}</version> </dependency> </dependencies> Part 3: Java Service Implementation
3.1 User Service with Kuma Integration
// File: user-service/src/main/java/com/example/user/UserServiceApplication.java package com.example.user; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication @EnableConfigurationProperties(UserServiceProperties.class) public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); } } // File: user-service/src/main/java/com/example/user/UserServiceProperties.java package com.example.user; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "user.service") public record UserServiceProperties( String name, String version, String environment, MeshProperties mesh ) { public record MeshProperties( boolean enabled, String zone, String serviceAccount, int proxyPort ) {} } // File: user-service/src/main/java/com/example/user/UserController.java package com.example.user; import io.micrometer.core.annotation.Timed; import io.micrometer.observation.annotation.Observed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @RestController @RequestMapping("/api/v1/users") public class UserController { private static final Logger logger = LoggerFactory.getLogger(UserController.class); private final Map<String, User> users = new ConcurrentHashMap<>(); private final UserServiceProperties properties; public UserController(UserServiceProperties properties) { this.properties = properties; // Initialize with sample data users.put("1", new User("1", "[email protected]", "John Doe", "ACTIVE")); users.put("2", new User("2", "[email protected]", "Jane Smith", "ACTIVE")); } @GetMapping @Timed(value = "user.service.get.all", description = "Get all users") @Observed(name = "user.get.all", contextualName = "user-service.get-all-users") public ResponseEntity<List<User>> getAllUsers() { logger.info("Fetching all users - Service: {}, Zone: {}", properties.name(), properties.mesh().zone()); return ResponseEntity.ok(List.copyOf(users.values())); } @GetMapping("/{userId}") @Timed(value = "user.service.get.by.id", description = "Get user by ID") public ResponseEntity<User> getUserById(@PathVariable String userId) { logger.info("Fetching user by ID: {} - Service: {}", userId, properties.name()); User user = users.get(userId); if (user == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(user); } @PostMapping @Timed(value = "user.service.create", description = "Create user") public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) { logger.info("Creating user: {} - Service: {}", request.email(), properties.name()); String userId = String.valueOf(users.size() + 1); User user = new User(userId, request.email(), request.name(), "ACTIVE"); users.put(userId, user); return ResponseEntity.ok(user); } @GetMapping("/health") public ResponseEntity<Map<String, String>> health() { return ResponseEntity.ok(Map.of( "status", "healthy", "service", properties.name(), "version", properties.version(), "zone", properties.mesh().zone(), "meshEnabled", String.valueOf(properties.mesh().enabled()) )); } @GetMapping("/metrics/info") public ResponseEntity<Map<String, Object>> metricsInfo() { return ResponseEntity.ok(Map.of( "service", properties.name(), "totalUsers", users.size(), "mesh", Map.of( "enabled", properties.mesh().enabled(), "zone", properties.mesh().zone(), "proxyPort", properties.mesh().proxyPort() ) )); } } // File: user-service/src/main/java/com/example/user/User.java package com.example.user; public record User( String id, String email, String name, String status ) {} // File: user-service/src/main/java/com/example/user/CreateUserRequest.java package com.example.user; public record CreateUserRequest( String email, String name ) {} 3.2 Order Service with Resilient Communication
// File: order-service/src/main/java/com/example/order/OrderServiceApplication.java package com.example.order; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableConfigurationProperties(OrderServiceProperties.class) public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } } // File: order-service/src/main/java/com/example/order/OrderController.java package com.example.order; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import io.github.resilience4j.retry.annotation.Retry; import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @RestController @RequestMapping("/api/v1/orders") public class OrderController { private static final Logger logger = LoggerFactory.getLogger(OrderController.class); private final Map<String, Order> orders = new ConcurrentHashMap<>(); private final OrderServiceProperties properties; private final WebClient webClient; public OrderController(OrderServiceProperties properties, WebClient.Builder webClientBuilder) { this.properties = properties; this.webClient = webClientBuilder.baseUrl("http://user-service:8080").build(); } @PostMapping @CircuitBreaker(name = "orderService", fallbackMethod = "createOrderFallback") @Retry(name = "orderService") @TimeLimiter(name = "orderService") public Mono<ResponseEntity<Order>> createOrder(@RequestBody CreateOrderRequest request) { logger.info("Creating order for user: {} - Service: {}", request.userId(), properties.name()); // Validate user exists via user service (through Kuma mesh) return validateUser(request.userId()) .flatMap(userValid -> { if (!userValid) { return Mono.just(ResponseEntity.badRequest() .body(new Order("invalid", "USER_NOT_FOUND", null, "FAILED"))); } String orderId = String.valueOf(orders.size() + 1); Order order = new Order(orderId, request.userId(), request.items(), "CREATED"); orders.put(orderId, order); logger.info("Order created successfully: {} - Service: {}", orderId, properties.name()); return Mono.just(ResponseEntity.ok(order)); }); } @GetMapping("/{orderId}") public ResponseEntity<Order> getOrder(@PathVariable String orderId) { logger.info("Fetching order: {} - Service: {}", orderId, properties.name()); Order order = orders.get(orderId); if (order == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(order); } @GetMapping public ResponseEntity<List<Order>> getAllOrders() { logger.info("Fetching all orders - Service: {}", properties.name()); return ResponseEntity.ok(List.copyOf(orders.values())); } @GetMapping("/health") public ResponseEntity<Map<String, String>> health() { return ResponseEntity.ok(Map.of( "status", "healthy", "service", properties.name(), "version", properties.version(), "zone", properties.mesh().zone() )); } private Mono<Boolean> validateUser(String userId) { return webClient.get() .uri("/api/v1/users/{userId}", userId) .retrieve() .toBodilessEntity() .map(response -> response.getStatusCode().is2xxSuccessful()) .doOnError(error -> logger.warn("User validation failed for {}: {}", userId, error.getMessage())) .onErrorReturn(false); } // Fallback method for circuit breaker public Mono<ResponseEntity<Order>> createOrderFallback(CreateOrderRequest request, Exception ex) { logger.warn("Order creation fallback triggered for user: {}, error: {}", request.userId(), ex.getMessage()); Order fallbackOrder = new Order("fallback", request.userId(), request.items(), "PENDING_VALIDATION"); return Mono.just(ResponseEntity.accepted().body(fallbackOrder)); } } // File: order-service/src/main/java/com/example/order/Order.java package com.example.order; import java.util.List; public record Order( String id, String userId, List<OrderItem> items, String status ) {} // File: order-service/src/main/java/com/example/order/CreateOrderRequest.java package com.example.order; import java.util.List; public record CreateOrderRequest( String userId, List<OrderItem> items ) {} // File: order-service/src/main/java/com/example/order/OrderItem.java package com.example.order; public record OrderItem( String productId, int quantity, double price ) {} // File: order-service/src/main/java/com/example/order/OrderServiceProperties.java package com.example.order; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "order.service") public record OrderServiceProperties( String name, String version, String environment, MeshProperties mesh ) { public record MeshProperties( boolean enabled, String zone, String serviceAccount, int proxyPort ) {} } Part 4: Kuma Configuration
4.1 Mesh Configuration
# kuma-config/mesh.yaml apiVersion: kuma.io/v1alpha1 kind: Mesh metadata: name: default spec: mtls: backends: - name: ca-1 type: builtin enabledBackend: ca-1 logging: backends: - name: file-backend type: file format: '{"start_time": "%START_TIME%", "method": "%REQ(:METHOD)%", "path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%", "protocol": "%PROTOCOL%", "response_code": "%RESPONSE_CODE%", "response_flags": "%RESPONSE_FLAGS%", "bytes_received": "%BYTES_RECEIVED%", "bytes_sent": "%BYTES_SENT%", "duration": "%DURATION%", "upstream_service": "%UPSTREAM_CLUSTER%", "upstream_host": "%UPSTREAM_HOST%", "x_forwarded_for": "%REQ(X-FORWARDED-FOR)%", "user_agent": "%REQ(USER-AGENT)%", "request_id": "%REQ(X-REQUEST-ID)%", "authority": "%REQ(:AUTHORITY)%", "upstream_local_address": "%UPSTREAM_LOCAL_ADDRESS%"}' file: path: /tmp/access.log tracing: backends: - name: jaeger-collector type: jaeger sampling: 100.0 conf: collectorEndpoint: http://jaeger-collector:14268/api/traces metrics: enabledBackend: prometheus-1 backends: - name: prometheus-1 type: prometheus conf: port: 5670 path: /metrics tags: kuma.io/service: dataplane_metrics 4.2 Traffic Route Configuration
# kuma-config/traffic-route.yaml apiVersion: kuma.io/v1alpha1 kind: TrafficRoute mesh: default metadata: name: route-all-traffic spec: sources: - match: kuma.io/service: '*' destinations: - match: kuma.io/service: '*' conf: http: - match: path: prefix: / destination: kuma.io/service: '*' --- apiVersion: kuma.io/v1alpha1 kind: TrafficRoute mesh: default metadata: name: canary-user-service spec: sources: - match: kuma.io/service: 'order-service' destinations: - match: kuma.io/service: 'user-service' conf: http: - match: headers: x-canary: 'true' split: - weight: 10 destination: kuma.io/service: 'user-service-v2' - weight: 90 destination: kuma.io/service: 'user-service' - match: path: prefix: '/api/v1/users' split: - weight: 100 destination: kuma.io/service: 'user-service'
4.3 Circuit Breaker Configuration
# kuma-config/circuit-breaker.yaml apiVersion: kuma.io/v1alpha1 kind: CircuitBreaker mesh: default metadata: name: order-service-cb spec: sources: - match: kuma.io/service: 'order-service' destinations: - match: kuma.io/service: 'user-service' conf: interval: 30s baseEjectionTime: 30s maxEjectionPercent: 50 thresholds: maxConnections: 100 maxRequests: 100 maxRetries: 3 maxPendingRequests: 50 detectors: totalErrors: consecutive: 5 gatewayErrors: consecutive: 10 localOriginErrors: consecutive: 7
4.4 Retry Policy
# kuma-config/retry-policy.yaml apiVersion: kuma.io/v1alpha1 kind: RetryPolicy mesh: default metadata: name: order-service-retries spec: sources: - match: kuma.io/service: 'order-service' destinations: - match: kuma.io/service: 'user-service' conf: http: numRetries: 3 perTryTimeout: 2s backOff: baseInterval: 0.1s maxInterval: 3s retryOn: - gateway-error - connect-failure - refused-stream - retriable-4xx - reset
4.5 Fault Injection for Testing
# kuma-config/fault-injection.yaml apiVersion: kuma.io/v1alpha1 kind: FaultInjection mesh: default metadata: name: user-service-delay spec: sources: - match: kuma.io/service: 'test-client' destinations: - match: kuma.io/service: 'user-service' conf: delay: percentage: 50 value: 5s abort: percentage: 10 httpStatus: 503
Part 5: Kubernetes Deployment
5.1 Kuma Installation
# kubernetes/kuma-install.yaml apiVersion: v1 kind: Namespace metadata: name: kuma-system --- apiVersion: apps/v1 kind: Deployment metadata: namespace: kuma-system name: kuma-control-plane labels: app: kuma-control-plane spec: replicas: 1 selector: matchLabels: app: kuma-control-plane template: metadata: labels: app: kuma-control-plane annotations: kuma.io/mesh: default spec: containers: - name: kuma-cp image: kong-docker-kuma-docker.bintray.io/kuma-cp:2.6.0 ports: - containerPort: 5681 - containerPort: 5682 env: - name: KUMA_MULTI_ZONE_GLOBAL_KDS_GRPC_PORT value: "5685" resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" --- apiVersion: v1 kind: Service metadata: namespace: kuma-system name: kuma-control-plane labels: app: kuma-control-plane spec: ports: - port: 5681 targetPort: 5681 name: grpc - port: 5682 targetPort: 5682 name: http selector: app: kuma-control-plane
5.2 User Service Deployment
# kubernetes/services/user-service.yaml apiVersion: apps/v1 kind: Deployment metadata: name: user-service namespace: default labels: app: user-service version: v1 spec: replicas: 3 selector: matchLabels: app: user-service version: v1 template: metadata: labels: app: user-service version: v1 annotations: kuma.io/mesh: default kuma.io/sidecar-injection: "enabled" prometheus.io/scrape: "true" prometheus.io/port: "5670" prometheus.io/path: "/metrics" spec: serviceAccountName: user-service containers: - name: user-service image: my-registry/user-service:1.0.0 ports: - containerPort: 8080 env: - name: SPRING_APPLICATION_NAME value: "user-service" - name: USER_SERVICE_NAME value: "user-service" - name: USER_SERVICE_VERSION value: "1.0.0" - name: USER_SERVICE_ENVIRONMENT value: "production" - name: USER_SERVICE_MESH_ENABLED value: "true" - name: USER_SERVICE_MESH_ZONE value: "us-west-2" resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "300m" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: user-service namespace: default labels: app: user-service spec: ports: - port: 80 targetPort: 8080 protocol: TCP name: http selector: app: user-service type: ClusterIP --- apiVersion: v1 kind: ServiceAccount metadata: name: user-service namespace: default
5.3 Order Service Deployment
# kubernetes/services/order-service.yaml apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: default labels: app: order-service version: v1 spec: replicas: 2 selector: matchLabels: app: order-service version: v1 template: metadata: labels: app: order-service version: v1 annotations: kuma.io/mesh: default kuma.io/sidecar-injection: "enabled" prometheus.io/scrape: "true" prometheus.io/port: "5670" spec: serviceAccountName: order-service containers: - name: order-service image: my-registry/order-service:1.0.0 ports: - containerPort: 8080 env: - name: SPRING_APPLICATION_NAME value: "order-service" - name: ORDER_SERVICE_NAME value: "order-service" - name: ORDER_SERVICE_VERSION value: "1.0.0" - name: ORDER_SERVICE_ENVIRONMENT value: "production" - name: ORDER_SERVICE_MESH_ENABLED value: "true" - name: ORDER_SERVICE_MESH_ZONE value: "us-west-2" resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "300m" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: order-service namespace: default labels: app: order-service spec: ports: - port: 80 targetPort: 8080 protocol: TCP name: http selector: app: order-service type: ClusterIP --- apiVersion: v1 kind: ServiceAccount metadata: name: order-service namespace: default
Part 6: Application Configuration
6.1 User Service Configuration
# user-service/src/main/resources/application.yaml spring: application: name: user-service server: port: 8080 user: service: name: user-service version: 1.0.0 environment: ${ENVIRONMENT:development} mesh: enabled: ${MESH_ENABLED:true} zone: ${MESH_ZONE:default} service-account: user-service proxy-port: 5670 management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always metrics: enabled: true metrics: export: prometheus: enabled: true tags: application: ${spring.application.name} zone: ${user.service.mesh.zone} environment: ${user.service.environment} logging: level: com.example.user: DEBUG org.springframework.web: INFO io.kuma: DEBUG resilience4j: circuitbreaker: instances: userService: register-health-indicator: true sliding-window-size: 10 minimum-number-of-calls: 5 permitted-number-of-calls-in-half-open-state: 3 automatic-transition-from-open-to-half-open-enabled: true wait-duration-in-open-state: 10s failure-rate-threshold: 50 event-consumer-buffer-size: 10 retry: instances: userService: max-attempts: 3 wait-duration: 2s 6.2 Order Service Configuration
# order-service/src/main/resources/application.yaml spring: application: name: order-service server: port: 8080 order: service: name: order-service version: 1.0.0 environment: ${ENVIRONMENT:development} mesh: enabled: ${MESH_ENABLED:true} zone: ${MESH_ZONE:default} service-account: order-service proxy-port: 5670 management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always metrics: enabled: true metrics: export: prometheus: enabled: true tags: application: ${spring.application.name} zone: ${order.service.mesh.zone} environment: ${order.service.environment} logging: level: com.example.order: DEBUG org.springframework.web: INFO io.kuma: DEBUG resilience4j: circuitbreaker: instances: orderService: register-health-indicator: true sliding-window-size: 10 minimum-number-of-calls: 5 permitted-number-of-calls-in-half-open-state: 3 automatic-transition-from-open-to-half-open-enabled: true wait-duration-in-open-state: 10s failure-rate-threshold: 50 retry: instances: orderService: max-attempts: 3 wait-duration: 1s webclient: timeout: connect: 5s read: 10s write: 10s Part 7: Observability and Monitoring
7.1 Custom Metrics and Health Checks
// File: user-service/src/main/java/com/example/user/UserServiceMetrics.java package com.example.user; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.springframework.stereotype.Component; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @Component public class UserServiceMetrics { private final MeterRegistry meterRegistry; private final Counter userCreationCounter; private final Counter userQueryCounter; private final Timer userQueryTimer; private final ConcurrentHashMap<String, Counter> errorCounters; public UserServiceMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.errorCounters = new ConcurrentHashMap<>(); this.userCreationCounter = Counter.builder("user.service.creation.total") .description("Total number of user creations") .tag("service", "user-service") .register(meterRegistry); this.userQueryCounter = Counter.builder("user.service.query.total") .description("Total number of user queries") .tag("service", "user-service") .register(meterRegistry); this.userQueryTimer = Timer.builder("user.service.query.duration") .description("Time taken for user queries") .tag("service", "user-service") .register(meterRegistry); } public void recordUserCreation() { userCreationCounter.increment(); } public void recordUserQuery() { userQueryCounter.increment(); } public Timer.Sample startQueryTimer() { return Timer.start(meterRegistry); } public void stopQueryTimer(Timer.Sample sample) { sample.stop(userQueryTimer); } public void recordError(String errorType) { Counter errorCounter = errorCounters.computeIfAbsent(errorType, key -> Counter.builder("user.service.errors.total") .description("Total number of errors by type") .tag("service", "user-service") .tag("error_type", errorType) .register(meterRegistry)); errorCounter.increment(); } public void recordMeshRequest(String destination, String status) { Counter.builder("user.service.mesh.requests.total") .description("Requests through service mesh") .tag("service", "user-service") .tag("destination", destination) .tag("status", status) .register(meterRegistry) .increment(); } } 7.2 Enhanced Health Indicator
// File: user-service/src/main/java/com/example/user/UserServiceHealthIndicator.java package com.example.user; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.stereotype.Component; import java.net.InetAddress; import java.util.HashMap; import java.util.Map; @Component public class UserServiceHealthIndicator implements HealthIndicator { private final UserServiceProperties properties; public UserServiceHealthIndicator(UserServiceProperties properties) { this.properties = properties; } @Override public Health health() { try { Map<String, Object> details = new HashMap<>(); details.put("service", properties.name()); details.put("version", properties.version()); details.put("environment", properties.environment()); details.put("meshEnabled", properties.mesh().enabled()); details.put("meshZone", properties.mesh().zone()); details.put("hostname", InetAddress.getLocalHost().getHostName()); details.put("timestamp", System.currentTimeMillis()); // Add mesh-specific health checks if (properties.mesh().enabled()) { details.put("meshStatus", "connected"); details.put("proxyPort", properties.mesh().proxyPort()); } return Health.up() .withDetails(details) .build(); } catch (Exception e) { return Health.down() .withDetail("error", e.getMessage()) .build(); } } } Part 8: Testing with Kuma
8.1 Integration Tests
// File: order-service/src/test/java/com/example/order/OrderServiceIntegrationTest.java package com.example.order; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") class OrderServiceIntegrationTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void shouldCreateOrderThroughMesh() { // Given String baseUrl = "http://localhost:" + port; CreateOrderRequest request = new CreateOrderRequest( "1", List.of(new OrderItem("product-1", 2, 29.99)) ); // When ResponseEntity<Order> response = restTemplate.postForEntity( baseUrl + "/api/v1/orders", request, Order.class); // Then assertTrue(response.getStatusCode().is2xxSuccessful()); assertNotNull(response.getBody()); assertEquals("1", response.getBody().userId()); assertNotNull(response.getBody().id()); } @Test void shouldReturnHealthStatusWithMeshInfo() { // Given String baseUrl = "http://localhost:" + port; // When ResponseEntity<String> response = restTemplate.getForEntity( baseUrl + "/api/v1/orders/health", String.class); // Then assertTrue(response.getStatusCode().is2xxSuccessful()); assertTrue(response.getBody().contains("healthy")); assertTrue(response.getBody().contains("order-service")); } } 8.2 Resilience Testing
// File: order-service/src/test/java/com/example/order/OrderServiceResilienceTest.java package com.example.order; import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @ActiveProfiles("test") class OrderServiceResilienceTest { @Autowired private CircuitBreakerRegistry circuitBreakerRegistry; @Test void shouldConfigureCircuitBreaker() { // When CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("orderService"); // Then assertNotNull(circuitBreaker); assertEquals("orderService", circuitBreaker.getName()); assertEquals(CircuitBreaker.State.CLOSED, circuitBreaker.getState()); } } Best Practices for Kuma with Java
- Gradual Adoption: Start with observability, then add security and traffic management
- Proper Tagging: Use consistent tags for services across the mesh
- Health Checks: Implement comprehensive health checks for both application and mesh
- Circuit Breaking: Combine application-level and mesh-level circuit breaking
- Observability: Leverage Kuma's built-in metrics, tracing, and logging
- Security: Enable mTLS for service-to-service communication
- Testing: Test both with and without mesh in your CI/CD pipeline
- Monitoring: Set up alerts for mesh-level and application-level issues
Conclusion
Integrating Kuma Service Mesh with Java applications provides a powerful foundation for building resilient, observable, and secure microservices architectures. By leveraging Kuma's capabilities alongside Java frameworks like Spring Boot, you can:
- Automate traffic management with sophisticated routing and load balancing
- Implement zero-trust security with automatic mTLS and service identity
- Gain comprehensive observability with built-in metrics, tracing, and logging
- Enhance resilience with circuit breaking, retries, and fault injection
- Simplify operations with declarative configuration and multi-zone support
The patterns and examples in this guide demonstrate how to effectively integrate Java microservices with Kuma service mesh, creating a robust platform for modern cloud-native applications that can scale across multiple environments and deployment scenarios.
x
Article
Kuma is an open-source, universal service mesh that runs on both Kubernetes and VMs. Built by Kong, Kuma provides a robust control plane that works with Envoy proxy to deliver security, reliability, and observability for microservices. For Java applications, Kuma offers seamless integration with minimal code changes while providing enterprise-grade service mesh capabilities.
In this guide, we'll explore how to integrate Java microservices with Kuma service mesh to achieve traffic management, security, observability, and resilience.
Why Kuma Service Mesh for Java?
- Universal Deployment: Works across Kubernetes, VMs, and containers
- Envoy-Powered: Leverages battle-tested Envoy proxy
- Multi-Zone Support: Native multi-zone and multi-cloud capabilities
- Zero-Trust Security: Built-in mTLS and service-to-service authentication
- Comprehensive Observability: Metrics, tracing, and logging out of the box
- Developer-Friendly: Easy to adopt with minimal application changes
Part 1: Kuma Architecture Overview
1.1 Kuma Components
Java Application → Kuma Data Plane (Envoy) → Kuma Control Plane → Management APIs ↓ ↓ ↓ ↓ Business Logic Traffic Routing Policies Kubernetes Spring Boot Security Configuration Docker Micronaut Observability Service Discovery VM Quarkus
1.2 Project Structure
java-kuma-mesh/ ├── user-service/ │ ├── src/main/java/com/example/user/ │ ├── Dockerfile │ └── kuma/ │ └── mesh.yaml ├── order-service/ │ ├── src/main/java/com/example/order/ │ └── Dockerfile ├── api-gateway/ │ ├── src/main/java/com/example/gateway/ │ └── Dockerfile ├── kuma-config/ │ ├── mesh.yaml │ ├── traffic-route.yaml │ └── circuit-breaker.yaml └── kubernetes/ ├── kuma-install.yaml └── services/ ├── user-service.yaml └── order-service.yaml
Part 2: Dependencies and Setup
2.1 Maven Dependencies
<!-- pom.xml --> <properties> <spring-boot.version>3.1.0</spring-boot.version> <micrometer.version>1.11.5</micrometer.version> <resilience4j.version>2.1.0</resilience4j.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Observability --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>${micrometer.version}</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId> <version>${micrometer.version}</version> </dependency> <!-- Resilience --> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> <version>${resilience4j.version}</version> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-circuitbreaker</artifactId> <version>${resilience4j.version}</version> </dependency> <!-- OpenTelemetry --> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-api</artifactId> <version>1.31.0</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <version>${spring-boot.version}</version> </dependency> </dependencies> Part 3: Java Service Implementation
3.1 User Service with Kuma Integration
// File: user-service/src/main/java/com/example/user/UserServiceApplication.java package com.example.user; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication @EnableConfigurationProperties(UserServiceProperties.class) public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); } } // File: user-service/src/main/java/com/example/user/UserServiceProperties.java package com.example.user; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "user.service") public record UserServiceProperties( String name, String version, String environment, MeshProperties mesh ) { public record MeshProperties( boolean enabled, String zone, String serviceAccount, int proxyPort ) {} } // File: user-service/src/main/java/com/example/user/UserController.java package com.example.user; import io.micrometer.core.annotation.Timed; import io.micrometer.observation.annotation.Observed; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @RestController @RequestMapping("/api/v1/users") public class UserController { private static final Logger logger = LoggerFactory.getLogger(UserController.class); private final Map<String, User> users = new ConcurrentHashMap<>(); private final UserServiceProperties properties; public UserController(UserServiceProperties properties) { this.properties = properties; // Initialize with sample data users.put("1", new User("1", "[email protected]", "John Doe", "ACTIVE")); users.put("2", new User("2", "[email protected]", "Jane Smith", "ACTIVE")); } @GetMapping @Timed(value = "user.service.get.all", description = "Get all users") @Observed(name = "user.get.all", contextualName = "user-service.get-all-users") public ResponseEntity<List<User>> getAllUsers() { logger.info("Fetching all users - Service: {}, Zone: {}", properties.name(), properties.mesh().zone()); return ResponseEntity.ok(List.copyOf(users.values())); } @GetMapping("/{userId}") @Timed(value = "user.service.get.by.id", description = "Get user by ID") public ResponseEntity<User> getUserById(@PathVariable String userId) { logger.info("Fetching user by ID: {} - Service: {}", userId, properties.name()); User user = users.get(userId); if (user == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(user); } @PostMapping @Timed(value = "user.service.create", description = "Create user") public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) { logger.info("Creating user: {} - Service: {}", request.email(), properties.name()); String userId = String.valueOf(users.size() + 1); User user = new User(userId, request.email(), request.name(), "ACTIVE"); users.put(userId, user); return ResponseEntity.ok(user); } @GetMapping("/health") public ResponseEntity<Map<String, String>> health() { return ResponseEntity.ok(Map.of( "status", "healthy", "service", properties.name(), "version", properties.version(), "zone", properties.mesh().zone(), "meshEnabled", String.valueOf(properties.mesh().enabled()) )); } @GetMapping("/metrics/info") public ResponseEntity<Map<String, Object>> metricsInfo() { return ResponseEntity.ok(Map.of( "service", properties.name(), "totalUsers", users.size(), "mesh", Map.of( "enabled", properties.mesh().enabled(), "zone", properties.mesh().zone(), "proxyPort", properties.mesh().proxyPort() ) )); } } // File: user-service/src/main/java/com/example/user/User.java package com.example.user; public record User( String id, String email, String name, String status ) {} // File: user-service/src/main/java/com/example/user/CreateUserRequest.java package com.example.user; public record CreateUserRequest( String email, String name ) {} 3.2 Order Service with Resilient Communication
// File: order-service/src/main/java/com/example/order/OrderServiceApplication.java package com.example.order; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableConfigurationProperties(OrderServiceProperties.class) public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } } // File: order-service/src/main/java/com/example/order/OrderController.java package com.example.order; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; import io.github.resilience4j.retry.annotation.Retry; import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @RestController @RequestMapping("/api/v1/orders") public class OrderController { private static final Logger logger = LoggerFactory.getLogger(OrderController.class); private final Map<String, Order> orders = new ConcurrentHashMap<>(); private final OrderServiceProperties properties; private final WebClient webClient; public OrderController(OrderServiceProperties properties, WebClient.Builder webClientBuilder) { this.properties = properties; this.webClient = webClientBuilder.baseUrl("http://user-service:8080").build(); } @PostMapping @CircuitBreaker(name = "orderService", fallbackMethod = "createOrderFallback") @Retry(name = "orderService") @TimeLimiter(name = "orderService") public Mono<ResponseEntity<Order>> createOrder(@RequestBody CreateOrderRequest request) { logger.info("Creating order for user: {} - Service: {}", request.userId(), properties.name()); // Validate user exists via user service (through Kuma mesh) return validateUser(request.userId()) .flatMap(userValid -> { if (!userValid) { return Mono.just(ResponseEntity.badRequest() .body(new Order("invalid", "USER_NOT_FOUND", null, "FAILED"))); } String orderId = String.valueOf(orders.size() + 1); Order order = new Order(orderId, request.userId(), request.items(), "CREATED"); orders.put(orderId, order); logger.info("Order created successfully: {} - Service: {}", orderId, properties.name()); return Mono.just(ResponseEntity.ok(order)); }); } @GetMapping("/{orderId}") public ResponseEntity<Order> getOrder(@PathVariable String orderId) { logger.info("Fetching order: {} - Service: {}", orderId, properties.name()); Order order = orders.get(orderId); if (order == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(order); } @GetMapping public ResponseEntity<List<Order>> getAllOrders() { logger.info("Fetching all orders - Service: {}", properties.name()); return ResponseEntity.ok(List.copyOf(orders.values())); } @GetMapping("/health") public ResponseEntity<Map<String, String>> health() { return ResponseEntity.ok(Map.of( "status", "healthy", "service", properties.name(), "version", properties.version(), "zone", properties.mesh().zone() )); } private Mono<Boolean> validateUser(String userId) { return webClient.get() .uri("/api/v1/users/{userId}", userId) .retrieve() .toBodilessEntity() .map(response -> response.getStatusCode().is2xxSuccessful()) .doOnError(error -> logger.warn("User validation failed for {}: {}", userId, error.getMessage())) .onErrorReturn(false); } // Fallback method for circuit breaker public Mono<ResponseEntity<Order>> createOrderFallback(CreateOrderRequest request, Exception ex) { logger.warn("Order creation fallback triggered for user: {}, error: {}", request.userId(), ex.getMessage()); Order fallbackOrder = new Order("fallback", request.userId(), request.items(), "PENDING_VALIDATION"); return Mono.just(ResponseEntity.accepted().body(fallbackOrder)); } } // File: order-service/src/main/java/com/example/order/Order.java package com.example.order; import java.util.List; public record Order( String id, String userId, List<OrderItem> items, String status ) {} // File: order-service/src/main/java/com/example/order/CreateOrderRequest.java package com.example.order; import java.util.List; public record CreateOrderRequest( String userId, List<OrderItem> items ) {} // File: order-service/src/main/java/com/example/order/OrderItem.java package com.example.order; public record OrderItem( String productId, int quantity, double price ) {} // File: order-service/src/main/java/com/example/order/OrderServiceProperties.java package com.example.order; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "order.service") public record OrderServiceProperties( String name, String version, String environment, MeshProperties mesh ) { public record MeshProperties( boolean enabled, String zone, String serviceAccount, int proxyPort ) {} } Part 4: Kuma Configuration
4.1 Mesh Configuration
# kuma-config/mesh.yaml apiVersion: kuma.io/v1alpha1 kind: Mesh metadata: name: default spec: mtls: backends: - name: ca-1 type: builtin enabledBackend: ca-1 logging: backends: - name: file-backend type: file format: '{"start_time": "%START_TIME%", "method": "%REQ(:METHOD)%", "path": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%", "protocol": "%PROTOCOL%", "response_code": "%RESPONSE_CODE%", "response_flags": "%RESPONSE_FLAGS%", "bytes_received": "%BYTES_RECEIVED%", "bytes_sent": "%BYTES_SENT%", "duration": "%DURATION%", "upstream_service": "%UPSTREAM_CLUSTER%", "upstream_host": "%UPSTREAM_HOST%", "x_forwarded_for": "%REQ(X-FORWARDED-FOR)%", "user_agent": "%REQ(USER-AGENT)%", "request_id": "%REQ(X-REQUEST-ID)%", "authority": "%REQ(:AUTHORITY)%", "upstream_local_address": "%UPSTREAM_LOCAL_ADDRESS%"}' file: path: /tmp/access.log tracing: backends: - name: jaeger-collector type: jaeger sampling: 100.0 conf: collectorEndpoint: http://jaeger-collector:14268/api/traces metrics: enabledBackend: prometheus-1 backends: - name: prometheus-1 type: prometheus conf: port: 5670 path: /metrics tags: kuma.io/service: dataplane_metrics 4.2 Traffic Route Configuration
# kuma-config/traffic-route.yaml apiVersion: kuma.io/v1alpha1 kind: TrafficRoute mesh: default metadata: name: route-all-traffic spec: sources: - match: kuma.io/service: '*' destinations: - match: kuma.io/service: '*' conf: http: - match: path: prefix: / destination: kuma.io/service: '*' --- apiVersion: kuma.io/v1alpha1 kind: TrafficRoute mesh: default metadata: name: canary-user-service spec: sources: - match: kuma.io/service: 'order-service' destinations: - match: kuma.io/service: 'user-service' conf: http: - match: headers: x-canary: 'true' split: - weight: 10 destination: kuma.io/service: 'user-service-v2' - weight: 90 destination: kuma.io/service: 'user-service' - match: path: prefix: '/api/v1/users' split: - weight: 100 destination: kuma.io/service: 'user-service'
4.3 Circuit Breaker Configuration
# kuma-config/circuit-breaker.yaml apiVersion: kuma.io/v1alpha1 kind: CircuitBreaker mesh: default metadata: name: order-service-cb spec: sources: - match: kuma.io/service: 'order-service' destinations: - match: kuma.io/service: 'user-service' conf: interval: 30s baseEjectionTime: 30s maxEjectionPercent: 50 thresholds: maxConnections: 100 maxRequests: 100 maxRetries: 3 maxPendingRequests: 50 detectors: totalErrors: consecutive: 5 gatewayErrors: consecutive: 10 localOriginErrors: consecutive: 7
4.4 Retry Policy
# kuma-config/retry-policy.yaml apiVersion: kuma.io/v1alpha1 kind: RetryPolicy mesh: default metadata: name: order-service-retries spec: sources: - match: kuma.io/service: 'order-service' destinations: - match: kuma.io/service: 'user-service' conf: http: numRetries: 3 perTryTimeout: 2s backOff: baseInterval: 0.1s maxInterval: 3s retryOn: - gateway-error - connect-failure - refused-stream - retriable-4xx - reset
4.5 Fault Injection for Testing
# kuma-config/fault-injection.yaml apiVersion: kuma.io/v1alpha1 kind: FaultInjection mesh: default metadata: name: user-service-delay spec: sources: - match: kuma.io/service: 'test-client' destinations: - match: kuma.io/service: 'user-service' conf: delay: percentage: 50 value: 5s abort: percentage: 10 httpStatus: 503
Part 5: Kubernetes Deployment
5.1 Kuma Installation
# kubernetes/kuma-install.yaml apiVersion: v1 kind: Namespace metadata: name: kuma-system --- apiVersion: apps/v1 kind: Deployment metadata: namespace: kuma-system name: kuma-control-plane labels: app: kuma-control-plane spec: replicas: 1 selector: matchLabels: app: kuma-control-plane template: metadata: labels: app: kuma-control-plane annotations: kuma.io/mesh: default spec: containers: - name: kuma-cp image: kong-docker-kuma-docker.bintray.io/kuma-cp:2.6.0 ports: - containerPort: 5681 - containerPort: 5682 env: - name: KUMA_MULTI_ZONE_GLOBAL_KDS_GRPC_PORT value: "5685" resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" --- apiVersion: v1 kind: Service metadata: namespace: kuma-system name: kuma-control-plane labels: app: kuma-control-plane spec: ports: - port: 5681 targetPort: 5681 name: grpc - port: 5682 targetPort: 5682 name: http selector: app: kuma-control-plane
5.2 User Service Deployment
# kubernetes/services/user-service.yaml apiVersion: apps/v1 kind: Deployment metadata: name: user-service namespace: default labels: app: user-service version: v1 spec: replicas: 3 selector: matchLabels: app: user-service version: v1 template: metadata: labels: app: user-service version: v1 annotations: kuma.io/mesh: default kuma.io/sidecar-injection: "enabled" prometheus.io/scrape: "true" prometheus.io/port: "5670" prometheus.io/path: "/metrics" spec: serviceAccountName: user-service containers: - name: user-service image: my-registry/user-service:1.0.0 ports: - containerPort: 8080 env: - name: SPRING_APPLICATION_NAME value: "user-service" - name: USER_SERVICE_NAME value: "user-service" - name: USER_SERVICE_VERSION value: "1.0.0" - name: USER_SERVICE_ENVIRONMENT value: "production" - name: USER_SERVICE_MESH_ENABLED value: "true" - name: USER_SERVICE_MESH_ZONE value: "us-west-2" resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "300m" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: user-service namespace: default labels: app: user-service spec: ports: - port: 80 targetPort: 8080 protocol: TCP name: http selector: app: user-service type: ClusterIP --- apiVersion: v1 kind: ServiceAccount metadata: name: user-service namespace: default
5.3 Order Service Deployment
# kubernetes/services/order-service.yaml apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: default labels: app: order-service version: v1 spec: replicas: 2 selector: matchLabels: app: order-service version: v1 template: metadata: labels: app: order-service version: v1 annotations: kuma.io/mesh: default kuma.io/sidecar-injection: "enabled" prometheus.io/scrape: "true" prometheus.io/port: "5670" spec: serviceAccountName: order-service containers: - name: order-service image: my-registry/order-service:1.0.0 ports: - containerPort: 8080 env: - name: SPRING_APPLICATION_NAME value: "order-service" - name: ORDER_SERVICE_NAME value: "order-service" - name: ORDER_SERVICE_VERSION value: "1.0.0" - name: ORDER_SERVICE_ENVIRONMENT value: "production" - name: ORDER_SERVICE_MESH_ENABLED value: "true" - name: ORDER_SERVICE_MESH_ZONE value: "us-west-2" resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "300m" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: order-service namespace: default labels: app: order-service spec: ports: - port: 80 targetPort: 8080 protocol: TCP name: http selector: app: order-service type: ClusterIP --- apiVersion: v1 kind: ServiceAccount metadata: name: order-service namespace: default
Part 6: Application Configuration
6.1 User Service Configuration
# user-service/src/main/resources/application.yaml spring: application: name: user-service server: port: 8080 user: service: name: user-service version: 1.0.0 environment: ${ENVIRONMENT:development} mesh: enabled: ${MESH_ENABLED:true} zone: ${MESH_ZONE:default} service-account: user-service proxy-port: 5670 management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always metrics: enabled: true metrics: export: prometheus: enabled: true tags: application: ${spring.application.name} zone: ${user.service.mesh.zone} environment: ${user.service.environment} logging: level: com.example.user: DEBUG org.springframework.web: INFO io.kuma: DEBUG resilience4j: circuitbreaker: instances: userService: register-health-indicator: true sliding-window-size: 10 minimum-number-of-calls: 5 permitted-number-of-calls-in-half-open-state: 3 automatic-transition-from-open-to-half-open-enabled: true wait-duration-in-open-state: 10s failure-rate-threshold: 50 event-consumer-buffer-size: 10 retry: instances: userService: max-attempts: 3 wait-duration: 2s 6.2 Order Service Configuration
# order-service/src/main/resources/application.yaml spring: application: name: order-service server: port: 8080 order: service: name: order-service version: 1.0.0 environment: ${ENVIRONMENT:development} mesh: enabled: ${MESH_ENABLED:true} zone: ${MESH_ZONE:default} service-account: order-service proxy-port: 5670 management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always metrics: enabled: true metrics: export: prometheus: enabled: true tags: application: ${spring.application.name} zone: ${order.service.mesh.zone} environment: ${order.service.environment} logging: level: com.example.order: DEBUG org.springframework.web: INFO io.kuma: DEBUG resilience4j: circuitbreaker: instances: orderService: register-health-indicator: true sliding-window-size: 10 minimum-number-of-calls: 5 permitted-number-of-calls-in-half-open-state: 3 automatic-transition-from-open-to-half-open-enabled: true wait-duration-in-open-state: 10s failure-rate-threshold: 50 retry: instances: orderService: max-attempts: 3 wait-duration: 1s webclient: timeout: connect: 5s read: 10s write: 10s Part 7: Observability and Monitoring
7.1 Custom Metrics and Health Checks
// File: user-service/src/main/java/com/example/user/UserServiceMetrics.java package com.example.user; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.springframework.stereotype.Component; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @Component public class UserServiceMetrics { private final MeterRegistry meterRegistry; private final Counter userCreationCounter; private final Counter userQueryCounter; private final Timer userQueryTimer; private final ConcurrentHashMap<String, Counter> errorCounters; public UserServiceMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.errorCounters = new ConcurrentHashMap<>(); this.userCreationCounter = Counter.builder("user.service.creation.total") .description("Total number of user creations") .tag("service", "user-service") .register(meterRegistry); this.userQueryCounter = Counter.builder("user.service.query.total") .description("Total number of user queries") .tag("service", "user-service") .register(meterRegistry); this.userQueryTimer = Timer.builder("user.service.query.duration") .description("Time taken for user queries") .tag("service", "user-service") .register(meterRegistry); } public void recordUserCreation() { userCreationCounter.increment(); } public void recordUserQuery() { userQueryCounter.increment(); } public Timer.Sample startQueryTimer() { return Timer.start(meterRegistry); } public void stopQueryTimer(Timer.Sample sample) { sample.stop(userQueryTimer); } public void recordError(String errorType) { Counter errorCounter = errorCounters.computeIfAbsent(errorType, key -> Counter.builder("user.service.errors.total") .description("Total number of errors by type") .tag("service", "user-service") .tag("error_type", errorType) .register(meterRegistry)); errorCounter.increment(); } public void recordMeshRequest(String destination, String status) { Counter.builder("user.service.mesh.requests.total") .description("Requests through service mesh") .tag("service", "user-service") .tag("destination", destination) .tag("status", status) .register(meterRegistry) .increment(); } } 7.2 Enhanced Health Indicator
// File: user-service/src/main/java/com/example/user/UserServiceHealthIndicator.java package com.example.user; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.stereotype.Component; import java.net.InetAddress; import java.util.HashMap; import java.util.Map; @Component public class UserServiceHealthIndicator implements HealthIndicator { private final UserServiceProperties properties; public UserServiceHealthIndicator(UserServiceProperties properties) { this.properties = properties; } @Override public Health health() { try { Map<String, Object> details = new HashMap<>(); details.put("service", properties.name()); details.put("version", properties.version()); details.put("environment", properties.environment()); details.put("meshEnabled", properties.mesh().enabled()); details.put("meshZone", properties.mesh().zone()); details.put("hostname", InetAddress.getLocalHost().getHostName()); details.put("timestamp", System.currentTimeMillis()); // Add mesh-specific health checks if (properties.mesh().enabled()) { details.put("meshStatus", "connected"); details.put("proxyPort", properties.mesh().proxyPort()); } return Health.up() .withDetails(details) .build(); } catch (Exception e) { return Health.down() .withDetail("error", e.getMessage()) .build(); } } } Part 8: Testing with Kuma
8.1 Integration Tests
// File: order-service/src/test/java/com/example/order/OrderServiceIntegrationTest.java package com.example.order; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") class OrderServiceIntegrationTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @Test void shouldCreateOrderThroughMesh() { // Given String baseUrl = "http://localhost:" + port; CreateOrderRequest request = new CreateOrderRequest( "1", List.of(new OrderItem("product-1", 2, 29.99)) ); // When ResponseEntity<Order> response = restTemplate.postForEntity( baseUrl + "/api/v1/orders", request, Order.class); // Then assertTrue(response.getStatusCode().is2xxSuccessful()); assertNotNull(response.getBody()); assertEquals("1", response.getBody().userId()); assertNotNull(response.getBody().id()); } @Test void shouldReturnHealthStatusWithMeshInfo() { // Given String baseUrl = "http://localhost:" + port; // When ResponseEntity<String> response = restTemplate.getForEntity( baseUrl + "/api/v1/orders/health", String.class); // Then assertTrue(response.getStatusCode().is2xxSuccessful()); assertTrue(response.getBody().contains("healthy")); assertTrue(response.getBody().contains("order-service")); } } 8.2 Resilience Testing
// File: order-service/src/test/java/com/example/order/OrderServiceResilienceTest.java package com.example.order; import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @ActiveProfiles("test") class OrderServiceResilienceTest { @Autowired private CircuitBreakerRegistry circuitBreakerRegistry; @Test void shouldConfigureCircuitBreaker() { // When CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("orderService"); // Then assertNotNull(circuitBreaker); assertEquals("orderService", circuitBreaker.getName()); assertEquals(CircuitBreaker.State.CLOSED, circuitBreaker.getState()); } } Best Practices for Kuma with Java
- Gradual Adoption: Start with observability, then add security and traffic management
- Proper Tagging: Use consistent tags for services across the mesh
- Health Checks: Implement comprehensive health checks for both application and mesh
- Circuit Breaking: Combine application-level and mesh-level circuit breaking
- Observability: Leverage Kuma's built-in metrics, tracing, and logging
- Security: Enable mTLS for service-to-service communication
- Testing: Test both with and without mesh in your CI/CD pipeline
- Monitoring: Set up alerts for mesh-level and application-level issues
Conclusion
Integrating Kuma Service Mesh with Java applications provides a powerful foundation for building resilient, observable, and secure microservices architectures. By leveraging Kuma's capabilities alongside Java frameworks like Spring Boot, you can:
- Automate traffic management with sophisticated routing and load balancing
- Implement zero-trust security with automatic mTLS and service identity
- Gain comprehensive observability with built-in metrics, tracing, and logging
- Enhance resilience with circuit breaking, retries, and fault injection
- Simplify operations with declarative configuration and multi-zone support
The patterns and examples in this guide demonstrate how to effectively integrate Java microservices with Kuma service mesh, creating a robust platform for modern cloud-native applications that can scale across multiple environments and deployment scenarios.
Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/
OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/
OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/
Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.
https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics
Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2
Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide
Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2
Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide
Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.
https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server
Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.