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:
- Cloud-native design with Kubernetes-native patterns
- Service discovery for microservices communication
- Dynamic configuration from ConfigMaps and Secrets
- Health checks and readiness/liveness probes
- Horizontal scaling with HPA
- Security with RBAC and service accounts
- 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.