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.