Iridium >> Compile-time Java
Note: Iridium is far from releasing and we're in need of maintainers. You can find my contact emails below :3
Introduction
I recently had an idea while visiting JavaForumNord . It came to me during a lecture about code reviewing. It didn't have anything to do with code reviewing, yet the idea came to me anyway.
All of us are familiar with frameworks like Spring, Quarkus, or Micronaut. I've built my own small Java framework before, similar to Spring, with runtime dependency injection and relatively slow startup times.
I wanted to do something similar to Quarkus and Micronaut: generate the bean wiring at compile time instead of doing it at runtime.
I cooked up a somewhat working proof-of-concept implementation in the following few hours. My proof of concept didn't have many features, but it worked. It was enough to prove that the idea could work. I had essentially built something similar to Micronaut.
Technical Details
Iridium is a framework that is similar to Micronaut, but with a few differences. Both frameworks wire their beans and routes at compile time using annotation processing.
What you write
@ConfigurationProperties("greeting")
public record GreetingConfig(String prefix) {
}
@Component
public final class GreetingService {
private final GreetingConfig config;
private final String suffix;
public GreetingService(
final GreetingConfig config,
final @Value("${greeting.suffix:!}") String suffix) {
this.config = config;
this.suffix = suffix;
}
public String greet(final String name) {
return config.prefix() + ", " + name + suffix;
}
@OnShutdown
public void stop() {
System.out.println("Goodbye!");
}
}
Code Generation Example
@Generated(
date = "2026-09-28T19:39:24.747375985Z",
author = "Iridium"
)
public final class BeanRegistrarGenerated implements BeanRegistrar {
@Override
public void register(BeanPool pool) {
pool.put(
"greetingConfig",
Config.string("greeting.prefix")
.map(GreetingConfig::new)
.unwrap()
);
pool.put(
"greetingService",
new GreetingService(
pool.get(GreetingConfig.class),
Config.string("greeting.suffix", "!")
.unwrap()
)
);
pool.put(
"helloController",
new HelloController(
pool.get(GreetingService.class)
)
);
pool.put(
"cc.asylum.iridium.demo.hello.GreetingService.stop",
new GreetingServiceStopHook()
);
}
}
@OnShutdown becomes the generated GreetingServiceStopHook at the bottom of the
registrar. Config keys and constructor arguments are inlined the same way.
The same processor path is used for HTTP routes. A class annotated with
@RestController and methods annotated with @GET
or @POST produces a generated route registrar, just like the
bean registrar above. Wiring and routing are both decided at compile time.
Code is generated using Java's annotation processing API. The annotation processing API allows you to react to certain annotations at compile time, giving you the ability to generate additional code. You cannot directly modify existing classes with a standard annotation processor; instead, you generate new classes.
Iridium uses annotation processors to generate wrapper classes around components. Components are
initialized as singletons, with their lifecycle being managed by the framework. This setup is determined at
compile time, so the necessary wrapper and initialization code can be generated ahead of runtime. DI is done
through constructors, the framework registers every bean inside the BeanPool and when initializing
Components it
fetches required beans from the BeanPool and initializes the Singleton with these Beans.
After generating these wrapper classes, the framework registers them as service providers in META-INF/services.
At runtime, the framework can then use Java's ServiceLoader API to discover and instantiate the
generated
providers.
Through this process the amount of reflection needed to implement underlying implementations is minimal. You
only need to use Java's ServiceLoader API to fetch generated providers. These will be initiated
immediately.
Through these optimizations startup time is being drastically reduced. Iridium also ships with a few
optimizations that make Iridium outclass frameworks like Quarkus and Spring Boot in speed and throughput. Instead
of relying on JPA (Hibernate) and Jackson which are reflection heavy, Iridium counts
on avaje-jsonb and jimmer.
These libraries do all the heavy lifting other libraries do in runtime during compile-time.
Benchmarks
| startup | RSS | quote p99 | order p99 | peak req/s | |
|---|---|---|---|---|---|
| Iridium | 663 ms | 154 MB | 2.4 ms | 3.1 ms | 35.6k |
| Quarkus 3.27 | 1369 ms | 214 MB | 5.9 ms | 4.1 ms | 34.7k |
| Spring Boot 3.5 | 2860 ms | 283 MB | 4.0 ms | 4.1 ms | 16.6k |
| Micronaut 4.9 | 1268 ms | 272 MB | 4.9 ms | 4.1 ms | 37.2k |
Alongside bean wiring and controller/route wiring Iridium also does DTO validation at compile time. Validators
are generated in compile-time making validation incredibly fast. Validation is as easy as ever, driven through
annotations and initiated through annotating your dto parameter in your controller method with
@Valid.
Validation Example
@Json
public record HelloDto(
@EscapeHtml @NotBlank String test,
@Email String test2) {
}
@POST("/echo")
public Response<HelloDto> echo(final @RequestBody @Valid HelloDto helloDto) {
return Response.ok(helloDto);
}
Users will automatically be informed about validation constraints.
{
"field":"email",
"message":"must be a valid email address",
"invalidValue":"notanemail.com"
}