Micronaut Kubernetes in Java: Cloud-Native Application Development

Micronaut is a modern JVM-based framework for building modular, easily testable microservices and serverless applications. When combined with Kubernetes, it provides a powerful platform for building cloud-native applications with minimal resource consumption and fast startup times.


Core Concepts

Why Micronaut for Kubernetes?

  • Low Memory Footprint: Uses compile-time dependency injection
  • Fast Startup Time: No reflection-based IoC at runtime
  • Native Kubernetes Integration: Service discovery, configuration, health checks
  • Reactive Programming: Built-in support for reactive streams
  • GraalVM Native Image: Can compile to native binaries

Key Kubernetes Integrations:

  • Service Discovery via Kubernetes API
  • Configuration from ConfigMaps and Secrets
  • Health Checks with Kubernetes probes
  • Distributed Tracing with Jaeger/Zipkin
  • Metrics with Micrometer and Prometheus

Dependencies and Setup

1. Maven Dependencies
<properties>
<micronaut.version>4.2.0</micronaut.version>
<micronaut.data.version>4.1.0</micronaut.version>
<jib.version>3.4.0</jib.version>
</properties>
<dependencies>
<!-- Micronaut Core -->
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject</artifactId>
<version>${micronaut.version}</version>
<scope>compile</scope>
</dependency>
<!-- Micronaut HTTP Server -->
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
<version>${micronaut.version}</version>
</dependency>
<!-- Micronaut Kubernetes -->
<dependency>
<groupId>io.micronaut.kubernetes</groupId>
<artifactId>micronaut-kubernetes-discovery-client</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>io.micronaut.kubernetes</groupId>
<artifactId>micronaut-kubernetes-informer</artifactId>
<version>4.1.0</version>
</dependency>
<!-- Micronaut Data -->
<dependency>
<groupId>io.micronaut.data</groupId>
<artifactId>micronaut-data-jdbc</artifactId>
<version>${micronaut.data.version}</version>
</dependency>
<!-- Micronaut Security -->
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-jwt</artifactId>
<version>${micronaut.version}</version>
</dependency>
<!-- Micronaut Micrometer -->
<dependency>
<groupId>io.micronaut.micrometer</groupId>
<artifactId>micronaut-micrometer-core</artifactId>
<version>${micronaut.version}</version>
</dependency>
<dependency>
<groupId>io.micronaut.micrometer</groupId>
<artifactId>micronaut-micrometer-registry-prometheus</artifactId>
<version>${micronaut.version}</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>io.micronaut.sql</groupId>
<artifactId>micronaut-jdbc-hikari</artifactId>
<version>${micronaut.version}</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-junit5</artifactId>
<version>${micronaut.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.micronaut.build</groupId>
<artifactId>micronaut-maven-plugin</artifactId>
<version>4.0.0</version>
</plugin>
<!-- Jib for Docker builds -->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>${jib.version}</version>
</plugin>
</plugins>
</build>
2. Micronaut Application Configuration
# src/main/resources/application.yml
micronaut:
application:
name: user-service
server:
port: 8080
router:
static-resources:
swagger:
paths: classpath:META-INF/swagger
mapping: /swagger/**
# Kubernetes Configuration
kubernetes:
client:
namespace: default
discovery:
enabled: true
service-mode: pod
includes: 
- auth-service
- product-service
# Security
security:
enabled: true
endpoints:
login:
enabled: true
oauth:
enabled: true
token:
jwt:
signatures:
secret:
generator:
secret: ${JWT_SECRET:pleaseChangeThisSecretForProduction}
# Metrics
metrics:
enabled: true
export:
prometheus:
enabled: true
step: PT1M
descriptions: true
# Tracing
tracing:
zipkin:
enabled: true
http:
url: http://zipkin:9411
# Database
datasources:
default:
url: ${DATASOURCE_URL:jdbc:postgresql://localhost:5432/users}
username: ${DATASOURCE_USERNAME:postgres}
password: ${DATASOURCE_PASSWORD:password}
driverClassName: org.postgresql.Driver
# Logging
logger:
levels:
io.micronaut: INFO
com.example: DEBUG

Core Application Implementation

1. Main Application Class
package com.example.userservice;
import io.micronaut.runtime.Micronaut;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
@OpenAPIDefinition(
info = @Info(
title = "User Service",
version = "1.0",
description = "User Management Microservice"
)
)
public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
}
2. Domain Models
package com.example.userservice.domain;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.data.annotation.*;
import io.micronaut.data.model.DataType;
import java.time.LocalDateTime;
import java.util.UUID;
@MappedEntity("users")
@Introspected
public class User {
@Id
@GeneratedValue(GeneratedValue.Type.UUID)
private UUID id;
@DateCreated
private LocalDateTime createdAt;
@DateUpdated
private LocalDateTime updatedAt;
@Column("email")
private String email;
@Column("first_name")
private String firstName;
@Column("last_name")
private String lastName;
@Column("status")
private UserStatus status;
@Column("metadata")
@TypeDef(type = DataType.JSON)
private UserMetadata metadata;
// Constructors
public User() {}
public User(String email, String firstName, String lastName) {
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
this.status = UserStatus.ACTIVE;
this.metadata = new UserMetadata();
}
// Getters and Setters
public UUID getId() { return id; }
public void setId(UUID id) { this.id = id; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public UserStatus getStatus() { return status; }
public void setStatus(UserStatus status) { this.status = status; }
public UserMetadata getMetadata() { return metadata; }
public void setMetadata(UserMetadata metadata) { this.metadata = metadata; }
}
@Introspected
class UserMetadata {
private String department;
private String role;
private boolean emailVerified;
private LocalDateTime lastLogin;
// Constructors, getters, setters
public UserMetadata() {
this.emailVerified = false;
}
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public boolean isEmailVerified() { return emailVerified; }
public void setEmailVerified(boolean emailVerified) { this.emailVerified = emailVerified; }
public LocalDateTime getLastLogin() { return lastLogin; }
public void setLastLogin(LocalDateTime lastLogin) { this.lastLogin = lastLogin; }
}
enum UserStatus {
ACTIVE, INACTIVE, SUSPENDED, PENDING
}
3. Repository Layer
package com.example.userservice.repository;
import com.example.userservice.domain.User;
import com.example.userservice.domain.UserStatus;
import io.micronaut.data.annotation.Join;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.repository.CrudRepository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface UserRepository extends CrudRepository<User, UUID> {
Optional<User> findByEmail(String email);
List<User> findByStatus(UserStatus status);
@Join(value = "metadata", type = Join.Type.LEFT_FETCH)
Page<User> findAll(Pageable pageable);
boolean existsByEmail(String email);
long countByStatus(UserStatus status);
List<User> findByLastNameContainingIgnoreCase(String lastName);
}
4. Service Layer
package com.example.userservice.service;
import com.example.userservice.domain.User;
import com.example.userservice.domain.UserStatus;
import com.example.userservice.repository.UserRepository;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.transaction.Transactional;
import java.util.Optional;
import java.util.UUID;
@Singleton
@Transactional
public class UserService {
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
private final UserRepository userRepository;
private final UserEventPublisher eventPublisher;
public UserService(UserRepository userRepository, UserEventPublisher eventPublisher) {
this.userRepository = userRepository;
this.eventPublisher = eventPublisher;
}
public Page<User> getAllUsers(Pageable pageable) {
LOG.debug("Fetching users page: {}", pageable);
return userRepository.findAll(pageable);
}
public Optional<User> getUserById(UUID id) {
LOG.debug("Fetching user by ID: {}", id);
return userRepository.findById(id);
}
public Optional<User> getUserByEmail(String email) {
LOG.debug("Fetching user by email: {}", email);
return userRepository.findByEmail(email);
}
public User createUser(User user) {
LOG.info("Creating new user with email: {}", user.getEmail());
if (userRepository.existsByEmail(user.getEmail())) {
throw new UserServiceException("User with email " + user.getEmail() + " already exists");
}
User savedUser = userRepository.save(user);
eventPublisher.publishUserCreated(savedUser);
return savedUser;
}
public User updateUser(UUID id, User userUpdate) {
LOG.info("Updating user: {}", id);
User existingUser = userRepository.findById(id)
.orElseThrow(() -> new UserServiceException("User not found: " + id));
// Update fields
if (userUpdate.getFirstName() != null) {
existingUser.setFirstName(userUpdate.getFirstName());
}
if (userUpdate.getLastName() != null) {
existingUser.setLastName(userUpdate.getLastName());
}
if (userUpdate.getStatus() != null) {
existingUser.setStatus(userUpdate.getStatus());
}
if (userUpdate.getMetadata() != null) {
existingUser.setMetadata(userUpdate.getMetadata());
}
User updatedUser = userRepository.update(existingUser);
eventPublisher.publishUserUpdated(updatedUser);
return updatedUser;
}
public void deleteUser(UUID id) {
LOG.info("Deleting user: {}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new UserServiceException("User not found: " + id));
userRepository.delete(user);
eventPublisher.publishUserDeleted(id);
}
public long getActiveUsersCount() {
return userRepository.countByStatus(UserStatus.ACTIVE);
}
@NonNull
public User suspendUser(UUID id) {
LOG.info("Suspending user: {}", id);
User user = userRepository.findById(id)
.orElseThrow(() -> new UserServiceException("User not found: " + id));
user.setStatus(UserStatus.SUSPENDED);
User suspendedUser = userRepository.update(user);
eventPublisher.publishUserSuspended(suspendedUser);
return suspendedUser;
}
}
class UserServiceException extends RuntimeException {
public UserServiceException(String message) {
super(message);
}
public UserServiceException(String message, Throwable cause) {
super(message, cause);
}
}
5. Event Publishing
package com.example.userservice.service;
import com.example.userservice.domain.User;
import io.micronaut.context.event.ApplicationEventPublisher;
import jakarta.inject.Singleton;
import java.util.UUID;
@Singleton
public class UserEventPublisher {
private final ApplicationEventPublisher<UserEvent> eventPublisher;
public UserEventPublisher(ApplicationEventPublisher<UserEvent> eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void publishUserCreated(User user) {
UserEvent event = UserEvent.userCreated(user);
eventPublisher.publishEvent(event);
}
public void publishUserUpdated(User user) {
UserEvent event = UserEvent.userUpdated(user);
eventPublisher.publishEvent(event);
}
public void publishUserDeleted(UUID userId) {
UserEvent event = UserEvent.userDeleted(userId);
eventPublisher.publishEvent(event);
}
public void publishUserSuspended(User user) {
UserEvent event = UserEvent.userSuspended(user);
eventPublisher.publishEvent(event);
}
}
class UserEvent {
private final String type;
private final User user;
private final UUID userId;
private final long timestamp;
private UserEvent(String type, User user, UUID userId) {
this.type = type;
this.user = user;
this.userId = userId;
this.timestamp = System.currentTimeMillis();
}
public static UserEvent userCreated(User user) {
return new UserEvent("USER_CREATED", user, user.getId());
}
public static UserEvent userUpdated(User user) {
return new UserEvent("USER_UPDATED", user, user.getId());
}
public static UserEvent userDeleted(UUID userId) {
return new UserEvent("USER_DELETED", null, userId);
}
public static UserEvent userSuspended(User user) {
return new UserEvent("USER_SUSPENDED", user, user.getId());
}
// Getters
public String getType() { return type; }
public User getUser() { return user; }
public UUID getUserId() { return userId; }
public long getTimestamp() { return timestamp; }
}

REST API Controllers

1. User Controller
package com.example.userservice.controller;
import com.example.userservice.domain.User;
import com.example.userservice.service.UserService;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.*;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.validation.Valid;
import java.net.URI;
import java.util.UUID;
@Controller("/api/users")
@Secured(SecurityRule.IS_AUTHENTICATED)
@Tag(name = "Users", description = "User Management API")
public class UserController {
private static final Logger LOG = LoggerFactory.getLogger(UserController.class);
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@Get
@Operation(summary = "Get all users", description = "Retrieve paginated list of users")
public Page<User> getAllUsers(
@Parameter(description = "Pagination parameters") 
@Nullable Pageable pageable) {
if (pageable == null) {
pageable = Pageable.from(0, 20);
}
LOG.info("Fetching users page: {}", pageable);
return userService.getAllUsers(pageable);
}
@Get("/{id}")
@Operation(summary = "Get user by ID", description = "Retrieve a specific user by their ID")
public HttpResponse<User> getUserById(
@Parameter(description = "User ID") UUID id) {
LOG.info("Fetching user by ID: {}", id);
return userService.getUserById(id)
.map(HttpResponse::ok)
.orElse(HttpResponse.notFound());
}
@Get("/email/{email}")
@Operation(summary = "Get user by email", description = "Retrieve a specific user by their email")
public HttpResponse<User> getUserByEmail(
@Parameter(description = "User email") String email) {
LOG.info("Fetching user by email: {}", email);
return userService.getUserByEmail(email)
.map(HttpResponse::ok)
.orElse(HttpResponse.notFound());
}
@Post
@Secured({"ROLE_ADMIN"})
@Operation(summary = "Create user", description = "Create a new user")
public HttpResponse<User> createUser(
@Parameter(description = "User data") @Body @Valid User user) {
LOG.info("Creating new user: {}", user.getEmail());
User createdUser = userService.createUser(user);
return HttpResponse.created(createdUser)
.headers(headers -> headers.location(URI.create("/api/users/" + createdUser.getId())));
}
@Put("/{id}")
@Secured({"ROLE_ADMIN"})
@Operation(summary = "Update user", description = "Update an existing user")
public HttpResponse<User> updateUser(
@Parameter(description = "User ID") UUID id,
@Parameter(description = "Updated user data") @Body @Valid User user) {
LOG.info("Updating user: {}", id);
User updatedUser = userService.updateUser(id, user);
return HttpResponse.ok(updatedUser);
}
@Delete("/{id}")
@Secured({"ROLE_ADMIN"})
@Operation(summary = "Delete user", description = "Delete a user")
public HttpResponse<Void> deleteUser(
@Parameter(description = "User ID") UUID id) {
LOG.info("Deleting user: {}", id);
userService.deleteUser(id);
return HttpResponse.noContent();
}
@Post("/{id}/suspend")
@Secured({"ROLE_ADMIN"})
@Operation(summary = "Suspend user", description = "Suspend a user account")
public HttpResponse<User> suspendUser(
@Parameter(description = "User ID") UUID id) {
LOG.info("Suspending user: {}", id);
User suspendedUser = userService.suspendUser(id);
return HttpResponse.ok(suspendedUser);
}
@Get("/stats/active-count")
@Operation(summary = "Get active users count", description = "Get the count of active users")
public ActiveUsersCountResponse getActiveUsersCount() {
long count = userService.getActiveUsersCount();
return new ActiveUsersCountResponse(count);
}
}
class ActiveUsersCountResponse {
private final long activeUsersCount;
public ActiveUsersCountResponse(long activeUsersCount) {
this.activeUsersCount = activeUsersCount;
}
public long getActiveUsersCount() {
return activeUsersCount;
}
}
2. Health and Metrics Controller
package com.example.userservice.controller;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.management.health.indicator.HealthResult;
import io.micronaut.management.health.indicator.annotation.Readiness;
import jakarta.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.Map;
@Controller("/health")
public class HealthController {
private static final Logger LOG = LoggerFactory.getLogger(HealthController.class);
private final DataSource dataSource;
@Inject
public HealthController(DataSource dataSource) {
this.dataSource = dataSource;
}
@Get
@Readiness
public HealthResult health() {
LOG.debug("Health check requested");
try (Connection connection = dataSource.getConnection()) {
boolean databaseHealthy = connection.isValid(5); // 5 second timeout
Map<String, Object> details = Map.of(
"database", databaseHealthy ? "UP" : "DOWN",
"timestamp", System.currentTimeMillis()
);
if (databaseHealthy) {
return HealthResult.builder("user-service")
.status(HealthResult.Status.UP)
.details(details)
.build();
} else {
return HealthResult.builder("user-service")
.status(HealthResult.Status.DOWN)
.details(details)
.build();
}
} catch (Exception e) {
LOG.error("Health check failed", e);
return HealthResult.builder("user-service")
.status(HealthResult.Status.DOWN)
.details(Map.of("error", e.getMessage()))
.build();
}
}
}

Kubernetes Integration

1. Kubernetes Service Discovery
package com.example.userservice.integration;
import com.example.userservice.domain.Product;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.retry.annotation.Retryable;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import java.util.List;
// Service Discovery Client for Product Service
@Client(id = "product-service", path = "/api/products")
@Retryable(attempts = "3", delay = "500ms")
public interface ProductServiceClient {
@Get("/user/{userId}")
@Header(name = "Authorization", value = "${product.service.api-key}")
Flowable<Product> getProductsByUser(String userId);
@Get("/{productId}")
@Header(name = "Authorization", value = "${product.service.api-key}")
Single<Product> getProductById(String productId);
}
// Service class using the discovery client
package com.example.userservice.service;
import com.example.userservice.integration.ProductServiceClient;
import com.example.userservice.domain.Product;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.exceptions.HttpStatusException;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.UUID;
@Singleton
public class ProductIntegrationService {
private static final Logger LOG = LoggerFactory.getLogger(ProductIntegrationService.class);
private final ProductServiceClient productServiceClient;
public ProductIntegrationService(ProductServiceClient productServiceClient) {
this.productServiceClient = productServiceClient;
}
public Flux<Product> getUserProducts(UUID userId) {
LOG.debug("Fetching products for user: {}", userId);
return Flux.from(productServiceClient.getProductsByUser(userId.toString()))
.onErrorResume(throwable -> {
LOG.error("Failed to fetch products for user {}: {}", userId, throwable.getMessage());
return Flux.empty();
});
}
public Product getProduct(String productId) {
LOG.debug("Fetching product: {}", productId);
return productServiceClient.getProductById(productId)
.onErrorReturn(throwable -> {
LOG.error("Failed to fetch product {}: {}", productId, throwable.getMessage());
return new HttpStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Product service unavailable");
})
.blockingGet();
}
}
2. Kubernetes Informer for ConfigMap Updates
package com.example.userservice.config;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.event.ApplicationEventPublisher;
import io.micronaut.kubernetes.client.v1.KubernetesClient;
import io.micronaut.kubernetes.client.v1.configmaps.ConfigMap;
import io.micronaut.kubernetes.client.v1.configmaps.ConfigMapWatch;
import io.micronaut.kubernetes.client.v1.configmaps.ConfigMapWatchEvent;
import io.micronaut.runtime.event.annotation.EventListener;
import io.micronaut.scheduling.annotation.Async;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
@Singleton
@Requires(bean = KubernetesClient.class)
public class ConfigMapWatcher {
private static final Logger LOG = LoggerFactory.getLogger(ConfigMapWatcher.class);
private final ApplicationEventPublisher<ConfigUpdatedEvent> eventPublisher;
public ConfigMapWatcher(ApplicationEventPublisher<ConfigUpdatedEvent> eventPublisher) {
this.eventPublisher = eventPublisher;
}
@EventListener
@Async
public void onConfigMapUpdate(ConfigMapWatchEvent event) {
ConfigMapWatch watch = event.getResult();
ConfigMap configMap = watch.getObject();
if (configMap != null && "user-service-config".equals(configMap.getMetadata().getName())) {
LOG.info("ConfigMap updated: {}", configMap.getMetadata().getName());
Map<String, String> newConfig = configMap.getData();
ConfigUpdatedEvent configEvent = new ConfigUpdatedEvent(newConfig);
eventPublisher.publishEvent(configEvent);
}
}
}
class ConfigUpdatedEvent {
private final Map<String, String> config;
private final long timestamp;
public ConfigUpdatedEvent(Map<String, String> config) {
this.config = config;
this.timestamp = System.currentTimeMillis();
}
public Map<String, String> getConfig() { return config; }
public long getTimestamp() { return timestamp; }
}
3. Dynamic Configuration from Kubernetes
package com.example.userservice.config;
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import java.util.Map;
@Singleton
@ConfigurationProperties("app")
@Requires(env = {Environment.KUBERNETES})
public class KubernetesAppConfig {
private static final Logger LOG = LoggerFactory.getLogger(KubernetesAppConfig.class);
private Map<String, String> features;
private Map<String, String> integrations;
private CacheConfig cache;
private SecurityConfig security;
@PostConstruct
public void init() {
LOG.info("Kubernetes App Config initialized");
LOG.debug("Features: {}", features);
LOG.debug("Integrations: {}", integrations);
}
// Getters and Setters
public Map<String, String> getFeatures() { return features; }
public void setFeatures(Map<String, String> features) { this.features = features; }
public Map<String, String> getIntegrations() { return integrations; }
public void setIntegrations(Map<String, String> integrations) { this.integrations = integrations; }
public CacheConfig getCache() { return cache; }
public void setCache(CacheConfig cache) { this.cache = cache; }
public SecurityConfig getSecurity() { return security; }
public void setSecurity(SecurityConfig security) { this.security = security; }
@ConfigurationProperties("cache")
public static class CacheConfig {
private boolean enabled;
private long ttl;
private int maxSize;
// Getters and Setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public long getTtl() { return ttl; }
public void setTtl(long ttl) { this.ttl = ttl; }
public int getMaxSize() { return maxSize; }
public void setMaxSize(int maxSize) { this.maxSize = maxSize; }
}
@ConfigurationProperties("security")
public static class SecurityConfig {
private boolean rateLimiting;
private int maxRequestsPerMinute;
private boolean corsEnabled;
// Getters and Setters
public boolean isRateLimiting() { return rateLimiting; }
public void setRateLimiting(boolean rateLimiting) { this.rateLimiting = rateLimiting; }
public int getMaxRequestsPerMinute() { return maxRequestsPerMinute; }
public void setMaxRequestsPerMinute(int maxRequestsPerMinute) { this.maxRequestsPerMinute = maxRequestsPerMinute; }
public boolean isCorsEnabled() { return corsEnabled; }
public void setCorsEnabled(boolean corsEnabled) { this.corsEnabled = corsEnabled; }
}
}

Kubernetes Deployment Manifests

1. Deployment YAML
# k8s/deployment.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
template:
metadata:
labels:
app: user-service
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: user-service-account
containers:
- name: user-service
image: my-registry/user-service:1.0.0
ports:
- containerPort: 8080
name: http
env:
- name: DATASOURCE_URL
valueFrom:
secretKeyRef:
name: user-service-secrets
key: datasource-url
- name: DATASOURCE_USERNAME
valueFrom:
secretKeyRef:
name: user-service-secrets
key: datasource-username
- name: DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: user-service-secrets
key: datasource-password
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: user-service-secrets
key: jwt-secret
- name: MICRONAUT_ENVIRONMENTS
value: "kubernetes"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
startupProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 30
---
apiVersion: v1
kind: Service
metadata:
name: user-service
namespace: default
labels:
app: user-service
spec:
selector:
app: user-service
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
type: ClusterIP
2. ConfigMap and Secrets
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: user-service-config
namespace: default
data:
application.yml: |
micronaut:
application:
name: user-service
metrics:
export:
prometheus:
enabled: true
app:
cache:
enabled: true
ttl: 3600000
maxSize: 1000
security:
rateLimiting: true
maxRequestsPerMinute: 1000
corsEnabled: true
features:
user-search: "enabled"
user-export: "disabled"
integrations:
product-service-timeout: "5000"
notification-service-retries: "3"
---
apiVersion: v1
kind: Secret
metadata:
name: user-service-secrets
namespace: default
type: Opaque
data:
datasource-url: "amRiYzpwb3N0Z3Jlc3FsOi8vdXNlci1kYi1zZXJ2aWNlOjU0MzIvdXNlcnM="
datasource-username: "cG9zdGdyZXM="
datasource-password: "cGFzc3dvcmQxMjM="
jwt-secret: "c3VwZXJTZWNyZXRLZXlGb3JKV1RTaWduaW5n"
3. Service Account and RBAC
# k8s/rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: user-service-account
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: user-service-role
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: user-service-binding
subjects:
- kind: ServiceAccount
name: user-service-account
namespace: default
roleRef:
kind: ClusterRole
name: user-service-role
apiGroup: rbac.authorization.k8s.io
4. Horizontal Pod Autoscaler
# k8s/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: user-service-hpa
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: user-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 60

Docker Configuration

1. Dockerfile with Multi-Stage Build
# Build stage
FROM eclipse-temurin:21-jdk-jammy as build
WORKDIR /app
# Copy Maven wrapper and pom.xml
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
# Download dependencies
RUN ./mvnw dependency:go-offline -B
# Copy source code
COPY src src
# Build application
RUN ./mvnw clean package -DskipTests
# Runtime stage
FROM eclipse-temurin:21-jre-jammy as runtime
# Install curl for health checks
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/* && \
apt-get clean
WORKDIR /app
# Create non-root user
RUN groupadd --system --gid 1000 appgroup && \
useradd --system --uid 1000 --gid appgroup appuser && \
chown -R appuser:appgroup /app
# Copy JAR from build stage
COPY --from=build --chown=appuser:appgroup /app/target/user-service-*.jar app.jar
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
2. Jib Configuration for Maven
<!-- Maven Jib Plugin Configuration -->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<from>
<image>eclipse-temurin:21-jre-jammy</image>
</from>
<to>
<image>my-registry/user-service:${project.version}</image>
<tags>
<tag>latest</tag>
</tags>
</to>
<container>
<user>1000:1000</user>
<ports>
<port>8080</port>
</ports>
<environment>
<JAVA_OPTS>-Xmx512m -Xms256m</JAVA_OPTS>
<MICRONAUT_ENVIRONMENTS>kubernetes</MICRONAUT_ENVIRONMENTS>
</environment>
<labels>
<version>${project.version}</version>
<maintainer>[email protected]</maintainer>
</labels>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
</container>
</configuration>
</plugin>

Testing

1. Unit Tests
package com.example.userservice.service;
import com.example.userservice.domain.User;
import com.example.userservice.domain.UserStatus;
import com.example.userservice.repository.UserRepository;
import io.micronaut.test.annotation.MockBean;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@MicronautTest
class UserServiceTest {
@Inject
UserService userService;
@Inject
UserRepository userRepository;
@Inject
UserEventPublisher eventPublisher;
@MockBean(UserRepository.class)
UserRepository userRepository() {
return Mockito.mock(UserRepository.class);
}
@MockBean(UserEventPublisher.class)
UserEventPublisher eventPublisher() {
return Mockito.mock(UserEventPublisher.class);
}
@Test
void testCreateUser() {
// Given
User user = new User("[email protected]", "John", "Doe");
User savedUser = new User("[email protected]", "John", "Doe");
savedUser.setId(UUID.randomUUID());
when(userRepository.existsByEmail("[email protected]")).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(savedUser);
// When
User result = userService.createUser(user);
// Then
assertNotNull(result);
assertNotNull(result.getId());
assertEquals("[email protected]", result.getEmail());
assertEquals(UserStatus.ACTIVE, result.getStatus());
verify(userRepository).existsByEmail("[email protected]");
verify(userRepository).save(any(User.class));
verify(eventPublisher).publishUserCreated(savedUser);
}
@Test
void testGetUserById() {
// Given
UUID userId = UUID.randomUUID();
User user = new User("[email protected]", "John", "Doe");
user.setId(userId);
when(userRepository.findById(userId)).thenReturn(Optional.of(user));
// When
Optional<User> result = userService.getUserById(userId);
// Then
assertTrue(result.isPresent());
assertEquals(userId, result.get().getId());
verify(userRepository).findById(userId);
}
@Test
void testCreateUserWithExistingEmail() {
// Given
User user = new User("[email protected]", "John", "Doe");
when(userRepository.existsByEmail("[email protected]")).thenReturn(true);
// When & Then
assertThrows(UserServiceException.class, () -> userService.createUser(user));
verify(userRepository, never()).save(any(User.class));
}
}
2. Integration Tests
package com.example.userservice.controller;
import com.example.userservice.domain.User;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@MicronautTest
class UserControllerTest {
@Inject
@Client("/")
HttpClient client;
@Test
void testGetUserById() {
// This would require test data setup
String userId = "test-uuid";
HttpResponse<User> response = client.toBlocking()
.exchange(HttpRequest.GET("/api/users/" + userId), User.class);
assertEquals(HttpStatus.OK, response.getStatus());
}
@Test
void testCreateUser() {
User newUser = new User("[email protected]", "Jane", "Smith");
HttpResponse<User> response = client.toBlocking()
.exchange(HttpRequest.POST("/api/users", newUser), User.class);
assertEquals(HttpStatus.CREATED, response.getStatus());
assertNotNull(response.body());
assertEquals("[email protected]", response.body().getEmail());
}
@Test
void testGetUserNotFound() {
String nonExistentId = "non-existent-uuid";
HttpClientResponseException exception = assertThrows(HttpClientResponseException.class, () -> {
client.toBlocking().exchange(HttpRequest.GET("/api/users/" + nonExistentId));
});
assertEquals(HttpStatus.NOT_FOUND, exception.getStatus());
}
}

Monitoring and Observability

1. Custom Metrics
package com.example.userservice.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import jakarta.inject.Singleton;
import java.util.concurrent.TimeUnit;
@Singleton
public class UserMetrics {
private final Counter userCreatedCounter;
private final Counter userUpdatedCounter;
private final Counter userDeletedCounter;
private final Timer userCreationTimer;
private final Timer userQueryTimer;
public UserMetrics(MeterRegistry meterRegistry) {
this.userCreatedCounter = Counter.builder("user.created")
.description("Number of users created")
.register(meterRegistry);
this.userUpdatedCounter = Counter.builder("user.updated")
.description("Number of users updated")
.register(meterRegistry);
this.userDeletedCounter = Counter.builder("user.deleted")
.description("Number of users deleted")
.register(meterRegistry);
this.userCreationTimer = Timer.builder("user.creation.time")
.description("Time taken to create a user")
.register(meterRegistry);
this.userQueryTimer = Timer.builder("user.query.time")
.description("Time taken to query users")
.register(meterRegistry);
}
public void recordUserCreated() {
userCreatedCounter.increment();
}
public void recordUserUpdated() {
userUpdatedCounter.increment();
}
public void recordUserDeleted() {
userDeletedCounter.increment();
}
public void recordUserCreationTime(long duration, TimeUnit unit) {
userCreationTimer.record(duration, unit);
}
public void recordUserQueryTime(long duration, TimeUnit unit) {
userQueryTimer.record(duration, unit);
}
}
2. Distributed Tracing
package com.example.userservice.tracing;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import jakarta.inject.Inject;
@Controller("/api/tracing")
public class TracingController {
@Inject
private Tracer tracer;
@Get("/demo")
public String tracingDemo() {
Span span = tracer.spanBuilder("tracing-demo-operation")
.startSpan();
try {
// Add attributes to span
span.setAttribute("user.id", "demo-user");
span.setAttribute("operation.type", "demo");
// Simulate some work
Thread.sleep(100);
return "Tracing demo completed";
} catch (InterruptedException e) {
span.recordException(e);
throw new RuntimeException("Operation interrupted", e);
} finally {
span.end();
}
}
}

Best Practices

1. Configuration Management
# application-kubernetes.yml
micronaut:
config-client:
enabled: true
kubernetes:
client:
namespace: ${KUBERNETES_NAMESPACE:default}
discovery:
enabled: true
config-maps:
enabled: true
caches:
config-map-cache:
expire-after-write: 5m
secrets:
enabled: true
endpoints:
health:
enabled: true
sensitive: false
details-visible: ANONYMOUS
metrics:
enabled: true
sensitive: false
loggers:
enabled: true
write-sensitive: false
management:
endpoints:
web:
exposure:
include: health,metrics,info,loggers
2. Resource Optimization
// Use @ExecuteOn for reactive operations
@Controller("/api/async-users")
@ExecuteOn(TaskExecutors.IO)
public class AsyncUserController {
private final UserService userService;
public AsyncUserController(UserService userService) {
this.userService = userService;
}
@Get
public Flowable<User> getUsersAsync() {
return Flowable.fromIterable(userService.getAllUsers(Pageable.unpaged()).getContent());
}
}

Conclusion

Building Micronaut applications for Kubernetes provides:

  • Fast startup times and low memory footprint
  • Native Kubernetes integration for service discovery and configuration
  • Reactive programming support for high concurrency
  • Comprehensive observability with metrics, tracing, and health checks
  • GraalVM native image support for extreme performance

Key benefits demonstrated:

  1. Cloud-native design with Kubernetes-native patterns
  2. Service discovery for microservices communication
  3. Dynamic configuration from ConfigMaps and Secrets
  4. Health checks and readiness/liveness probes
  5. Horizontal scaling with HPA
  6. Security with RBAC and service accounts
  7. Monitoring with Prometheus metrics and distributed tracing

This architecture enables building highly scalable, resilient, and observable microservices that fully leverage the Kubernetes ecosystem while maintaining Java's type safety and rich ecosystem.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper