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.