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
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.
| Setup | Wall-clock (ms) | total_ns (ms) | Style |
|---|---|---|---|
jvm baseline (java -version) | 104 | — | — |
| plain (no DI) | 172 | 36 | floor reference |
| dagger | 186 | 44 | compile-time, lazy |
| tiko | 202 | 61 | compile-time, lazy |
| avaje | 228 | 105 | compile-time, eager |
| hk2 | 307 | 159 | runtime, reflection, lazy |
| guice | 373 | 230 | runtime, reflection, lazy |
| micronaut (inject-only) | 459 | 308 | compile-time, eager + AOP |
| spring | 529 | 368 | runtime, 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.