Micronaut's pitch is simple: everything Spring does with runtime reflection, Micronaut does at compile time. No classpath scanning. No runtime proxy generation. No reflection-based dependency injection. The result is fast startup, low memory, and ahead-of-time compilation that actually works.

I built a service with Micronaut to see if the pitch holds up. It does. But there are tradeoffs that the marketing doesn't emphasize.

Compile-Time DI: How It Works

In Spring, when the application starts, it scans the classpath for annotated classes, resolves dependencies, creates proxies, and wires everything together. This is flexible - you can change behavior at runtime - but it's slow and memory-intensive.

Micronaut generates the DI wiring at compile time using annotation processors. When you compile your code, Micronaut generates $BeanDefinition classes that contain all the injection logic. At runtime, it just loads these pre-computed definitions. No scanning. No reflection.

@Singleton
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentGateway paymentGateway;

    public OrderService(OrderRepository orderRepository,
                        PaymentGateway paymentGateway) {
        this.orderRepository = orderRepository;
        this.paymentGateway = paymentGateway;
    }

    public Order createOrder(CreateOrderCommand command) {
        // business logic
    }
}

Looks identical to Spring. The difference is invisible - it's in the generated bytecode. During compilation, Micronaut generates a class that knows exactly how to instantiate OrderService with its dependencies, without reflection.

Beans and Scopes

Micronaut supports the standard scopes:

@Singleton           // one instance per application
@RequestScope        // one instance per HTTP request
@Prototype           // new instance every injection
@Infrastructure      // loaded eagerly at startup
@Context             // loaded eagerly, available in ApplicationContext

Conditional beans:

@Singleton
@Requires(property = "features.payment.enabled", value = "true")
public class StripePaymentGateway implements PaymentGateway {
    // only created when feature flag is true
}

@Singleton
@Requires(missingBeans = PaymentGateway.class)
public class NoOpPaymentGateway implements PaymentGateway {
    // fallback if no other PaymentGateway exists
}

This is Micronaut's version of Spring's @ConditionalOnProperty and @ConditionalOnMissingBean. The conditions are evaluated at compile time when possible, further reducing startup overhead.

Building an HTTP Server

@Controller("/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @Get("/{id}")
    public Order getOrder(String id) {
        return orderService.findById(id)
            .orElseThrow(() -> new OrderNotFoundException(id));
    }

    @Post
    @Status(HttpStatus.CREATED)
    public Order createOrder(@Body @Valid CreateOrderRequest request) {
        return orderService.createOrder(request.toCommand());
    }

    @Get
    public Page<Order> listOrders(@QueryValue Optional<String> status,
                                   Pageable pageable) {
        return orderService.findAll(status.orElse(null), pageable);
    }
}

The annotations are Micronaut's own, not JAX-RS and not Spring MVC. They're close enough that the learning curve is minimal. The Netty-based HTTP server starts in about 500ms on JVM.

Security

Micronaut Security provides authentication and authorization:

@Singleton
public class AuthenticationProviderUserPassword
    implements HttpRequestAuthenticationProvider<HttpRequest<?>> {

    @Override
    public AuthenticationResponse authenticate(HttpRequest<?> request,
                                                AuthenticationRequest<String, String> authRequest) {
        if (isValidUser(authRequest.getIdentity(), authRequest.getSecret())) {
            return AuthenticationResponse.success(authRequest.getIdentity(),
                List.of("ROLE_USER"));
        }
        return AuthenticationResponse.failure("Invalid credentials");
    }
}
@Controller("/admin")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class AdminController {

    @Get("/users")
    @Secured({"ROLE_ADMIN"})
    public List<User> listUsers() {
        // only accessible to admins
    }
}

JWT support is built in. OAuth2/OpenID Connect integration exists. It's not as feature-rich as Spring Security, but for standard JWT-based authentication in microservices, it covers the use cases.

Reactive Support

Micronaut's HTTP server is Netty-based and non-blocking by default. Returning reactive types works natively:

@Get("/{id}")
public Mono<Order> getOrder(String id) {
    return orderRepository.findById(id);
}

@Get
public Flux<Order> streamOrders() {
    return orderRepository.findAll();
}

Micronaut also supports Kotlin coroutines:

@Get("/{id}")
suspend fun getOrder(id: String): Order {
    return orderRepository.findById(id)
        ?: throw OrderNotFoundException(id)
}

The coroutine support is first-class, which brings us to an interesting use case.

Kotlin Microservices

Micronaut's compile-time DI plays well with Kotlin. No runtime reflection means Kotlin's sealed classes, data classes, and null safety work without surprises:

@Singleton
class OrderService(
    private val orderRepository: OrderRepository,
    private val eventPublisher: EventPublisher
) {
    fun createOrder(command: CreateOrderCommand): Order {
        val order = Order.create(command)
        val saved = orderRepository.save(order)
        eventPublisher.publish(OrderCreatedEvent(saved.id))
        return saved
    }

    fun findById(id: String): Order =
        orderRepository.findById(id)
            ?: throw OrderNotFoundException(id)
}

Kotlin + Micronaut + coroutines is a genuinely pleasant development experience. The compile-time DI means no kotlin-reflect dependency and no proxy issues with Kotlin's final-by-default classes.

Kafka Integration

Micronaut has native Kafka support:

@KafkaClient
public interface OrderEventProducer {

    @Topic("order-events")
    void sendOrderEvent(@KafkaKey String orderId, OrderEvent event);
}

@KafkaListener(groupId = "order-processor")
public class OrderEventConsumer {

    @Topic("order-events")
    public void receive(@KafkaKey String orderId,
                        OrderEvent event,
                        @Header("X-Correlation-Id") String correlationId) {
        // process event
    }
}

The @KafkaClient interface is implemented at compile time. No runtime proxy. The consumer is straightforward and supports batch processing, error handling, and manual offset management.

Kubernetes Support

Micronaut has built-in Kubernetes service discovery and configuration:

micronaut:
  application:
    name: order-service
  kubernetes:
    client:
      discovery:
        enabled: true
        mode: endpoint

With this, Micronaut discovers other services via Kubernetes endpoints directly. No Eureka, no Consul, no additional service registry. It uses the Kubernetes API as the service registry, which is exactly what you want in a Kubernetes environment.

Health checks, readiness probes, and liveness probes are built in:

@Singleton
@Readiness
public class DatabaseHealthIndicator implements HealthIndicator {

    @Override
    public Publisher<HealthResult> getResult() {
        // check database connectivity
        return Mono.just(HealthResult.builder("db", HealthStatus.UP).build());
    }
}

GraphQL Gateway

Micronaut GraphQL with federation can serve as an API gateway:

@GraphQLRootResolver
public class OrderQueryResolver {

    private final OrderService orderService;

    public Order order(String id) {
        return orderService.findById(id);
    }

    public List<Order> orders(String customerId) {
        return orderService.findByCustomerId(customerId);
    }
}

The GraphQL server starts fast (compile-time wiring, no reflection), which makes it suitable as a lightweight gateway. Combined with Micronaut's HTTP client:

@Client("customer-service")
public interface CustomerClient {

    @Get("/customers/{id}")
    Customer getCustomer(String id);
}

The HTTP client interface is implemented at compile time. The gateway stitches together data from multiple Micronaut services, each discoverable via Kubernetes.

The Verdict

Micronaut is impressive technology. Compile-time DI is the right idea - doing work at build time instead of runtime makes everything faster. The Kotlin support is excellent. The Kubernetes integration is first-class. The startup time and memory footprint rival Quarkus.

The tradeoff is ecosystem size. Spring Boot has a library for everything. Micronaut has libraries for the common things. If you need an obscure integration, you might be writing it yourself.

For new microservices in a Kubernetes environment, especially if your team uses Kotlin, Micronaut is a strong choice. For migrating existing Spring Boot services, the effort is significant - different annotations, different DI model, different testing approach. The performance gains are real but rarely justify a rewrite.

What I respect about Micronaut is the clarity of its thesis: reflection is a runtime cost that should be a build time cost. They're right. The question is whether the ecosystem around that thesis is mature enough for your needs. For most common patterns, it is.