Tiko: A compile-time orchestrator for Java

Tiko orchestrates, it doesn't bundle — direct access, compile-time safe, nothing wrapped.

Every Java LTS from 21, plus every GA since the latest LTS · today 21 · 25 · 26 · 27

Repo Bus Service Pool ???

What Tiko is

The three buckets

The container, the scopes, the event bus and the wiring graph ship as the framework. Every concern in a Tiko-built service lands in exactly one bucket.

Core — what Tiko ships

DI container, scopes (SINGLETON / EVENT / PROTOTYPE), event bus, @EventHandler + @EventTrigger, compile-time validation, lifecycle hooks, typed @Configuration records.

Plug in — you bring the library

HTTP, DataSource, migrations, caching, templates, security, any SDK client. Declare a @Produces method, return the library's value, consume it as a constructor parameter. No wrapper, no adapter layer.

Open design questions

Async generalisation, scheduling as a tick event, retry as an event loop — the small set of unresolved questions about extending the event model itself.

30-second taste

Plain classes, constructor injection

The annotation processor validates all dependencies at compile time and generates the wiring code — plain Java you can read and step through. No reflection, no classpath scanning in your wiring.

@Component(scope = Scope.SINGLETON)
public class UserRepository {
    public User findById(String id) { /* ... */ return null; }
}

@Component(scope = Scope.SINGLETON)
public class UserService {
    private final UserRepository repository;

    @Inject
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
}

try (Container container = Tiko.create()) {
    UserService service = container.get(UserService.class);
}

Compile-time safe

Wiring mistakes fail the build, not production

The processor checks the whole graph — unresolved and ambiguous dependencies, cycles, scope violations — and tells you how to fix it. This is the error Tiko reports for exactly this code, trimmed to the message and its suggested fixes.

OrderService.java

package demo;

import io.tiko.Scope;
import io.tiko.annotations.Component;
import io.tiko.annotations.Inject;

@Component(scope = Scope.SINGLETON)
public class OrderService {
    @Inject
    public OrderService(PaymentGateway gateway) {}
}

PaymentGateway.java

package demo;

public interface PaymentGateway {
    void charge(String orderId, long cents);
}

javac

OrderService.java:8: error: Cannot resolve dependency: demo.PaymentGateway

  Suggested fixes:
  1. Add a @Component class of type demo.PaymentGateway
  2. Create a @Produces factory method that returns demo.PaymentGateway
  3. Check if the dependency has the correct qualifier (@Named)

Plug in any library

Tiko owns the seam, never the library

A @Produces method hands the container a real HikariCP pool; your repository takes a plain javax.sql.DataSource. Nothing is wrapped — you call the driver directly. From tiko-examples/15_quickstart.

DataSourceFactory.java

@Component(scope = Scope.SINGLETON)
public class DataSourceFactory {

    private final AppConfig config;

    @Inject
    public DataSourceFactory(AppConfig config) {
        this.config = config;
    }

    @Produces(scope = Scope.SINGLETON)
    public DataSource dataSource() {
        var hc = new HikariConfig();
        hc.setJdbcUrl(config.db().url());
        hc.setUsername(config.db().user());
        hc.setPassword(config.db().password());
        hc.setMaximumPoolSize(config.db().poolSize());
        return new HikariDataSource(hc);
    }
}

NoteRepository.java

@Component(scope = Scope.SINGLETON)
public class NoteRepository {

    private final DataSource ds;

    @Inject
    public NoteRepository(DataSource ds) {
        this.ds = ds;
    }

    // raw JDBC against the pool: ds.getConnection() ...
}

Events chain

A handler's return value is the next event

@EventTrigger publishes what the handler returns; a guard decides whether the chain continues. Routing is by payload type, checked at compile time. From tiko-examples/03_events (logging trimmed).

@Component(scope = Scope.SINGLETON)
public class OrderWorkflow {

    @EventHandler
    @EventTrigger(eventName = "OrderValidated")
    public OrderValidated onPlaced(OrderPlaced event) {
        boolean valid = event.total() > 0;
        return new OrderValidated(event.orderId(), event.customer(), event.total(), valid);
    }

    @EventHandler
    @EventTrigger(eventName = "OrderShipped", guard = ValidOrderGuard.class)
    public OrderShipped onValidated(OrderValidated event) {
        return new OrderShipped(event.orderId(), event.customer(), event.total());
    }
}

Under the hood

Generated code you can step through

For the 30-second taste above, the processor emits a factory like this (excerpt). It's ordinary Java — set a breakpoint in it.

@Generated("io.tiko.processor.generator.ComponentFactoryGenerator")
public final class UserServiceFactory {
  // ...
  public UserService create() {
    UserRepository repository = container.options().resolveOverride(UserRepository.class, () -> container.getUserRepository());
    UserService instance = new UserService(repository);
    return instance;
  }
}

Measured cold start

The same four-singleton, two-module workload in eight setups. Median of 10 cold JVM invocations, default JVM and GC, Java 21, on a development laptop. These numbers move on different hardware — re-run locally before drawing conclusions. total_ns sums create + first gets + close.

SetupWall-clock (ms)total_ns (ms)Style
jvm baseline (java -version)104——
plain (no DI)17236floor reference
dagger18644compile-time, lazy
tiko20261compile-time, lazy
avaje228105compile-time, eager
hk2307159runtime, reflection, lazy
guice373230runtime, reflection, lazy
micronaut (inject-only)459308compile-time, eager + AOP
spring529368runtime, reflection, eager

The honest reading: the dominant axis is lazy vs eager init, not "compile-time vs runtime." Four clusters emerge — lean compile-time-lazy (plain, Dagger, Tiko at 36–61 ms total_ns), compile-time-eager (Avaje at 105 ms), runtime-reflection-lazy (HK2, Guice at 159–230 ms), and eager-with-overhead (Micronaut, Spring at 308–368 ms). Within each laziness class the compile-time framework is cheaper (Tiko < Guice; Avaje < Spring), but Avaje (compile-time + eager) is slower than HK2 and Guice (runtime + lazy) — eagerness costs more than reflection saves at this scale.

Methodology, per-phase tables and reproduction: comparisons/README.md.

Get started

Install from Maven Central

Import the BOM once; the modules and the annotation processor take their version from it. Supported JDKs: every Java LTS from 21, plus every GA since the latest LTS — today 21 · 25 · 26 · 27.

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.github.tomas-samek</groupId>
            <artifactId>tiko-bom</artifactId>
            <version>0.5.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>io.github.tomas-samek</groupId>
        <artifactId>tiko-api</artifactId>
    </dependency>
    <dependency>
        <groupId>io.github.tomas-samek</groupId>
        <artifactId>tiko-runtime</artifactId>
    </dependency>
    <!-- Optional, only if you use @Configuration -->
    <dependency>
        <groupId>io.github.tomas-samek</groupId>
        <artifactId>tiko-config</artifactId>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.13.0</version>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>io.github.tomas-samek</groupId>
                        <artifactId>tiko-processor</artifactId>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

On JDK 23+, javac runs annotation processing only when asked — the snippet above already does (it needs maven-compiler-plugin ≥ 3.13.0). Gradle and plain javac: jdk-23-setup.md. Starting fresh? The Maven archetype scaffolds a runnable project.