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
startup latency-vs-concurrency throughput-keepalive

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"
}

Sources

iridium-framework

Contact

yyuh@iridium4j.io

yyuh@femboys-are-hot.xyz