wakuwaku

Coming from Spring

How Spring concepts such as beans, constructor injection, @Scheduled, @KafkaListener, and @Transactional map onto wakuwaku, and where the two differ.

If you have built services with Spring Boot, many wakuwaku concepts will look familiar: a registry of shared components, components built from their dependencies, scheduled jobs, message listeners, repositories, validation. The difference is in when things are checked. Spring assembles the application at startup through reflection. wakuwaku assembles it through types, so most mistakes Spring reports when the application starts are compile errors in wakuwaku.

The Java snippets on this page are illustrations only. They show the Spring idiom being compared, not complete programs.

Concept map

SpringwakuwakuNotes
ApplicationContext, @Configuration + @BeanServiceBuilder::new(..).push(..)You build the registry yourself in main. Lookup is by type, resolved by the compiler.
Constructor injectionServiceCreation::DepA component built from one registered dependency names it as Dep. A component that needs several is built by a closure: compose_cron_job, compose_grpc_service, compose_iggy_consumer.
@Component scanningnoneEvery component is registered explicitly.
@Value, @ConfigurationPropertiesnoneBuild a config struct yourself and register it like any other dependency.
@ScheduledCronJob::INTERVALA CronJobInterval: AfterFinish ≈ fixedDelay, AtLeast ≈ fixedRate (runs never overlap), AfterStart ≈ fixedRate + @Async (runs may overlap). No cron expressions.
@KafkaListenerEventHandler + ServiceBuilder::iggy_consumerThe broker is Apache Iggy, not Kafka. A handler receives a batch of events of one key.
KafkaTemplate.sendPublisher::publishThe event's key picks the topic and the partition.
Spring Data repository methodsQuery values implementing Query + ExecuteEach query is a struct that declares the entities it reads and writes. No ORM, no derived queries.
@Transactional(readOnly = true)Db<S, ReadOnly>A capability of the handle, checked at compile time. See below.
@TransactionalTransaction + TxFlowThe transaction is a value: a flow whose steps run queries, driven by the library, not a proxy around a method.
JPA entity listeners, @TransactionalEventListenerBeforeWrite / AfterWrite hooksHooks wrap a handle and run around write queries.
Bean Validation (@Valid, @NotBlank)wakuwaku::proofA check returns Proven<T, P>, and functions that need valid input take that type.
@CacheableRedis CacheNot annotation-driven: you call get and put yourself, as you would with Spring's Cache or RedisTemplate.
ShedLock (@SchedulerLock)Redis LockLease-based and non-blocking: acquiring returns a token or None. Take it inside the job's run.
grpc-spring-boot-starter (@GrpcService)ServiceBuilder::grpc_serviceBuilds tonic servers from the registry. You start the tonic server yourself.
SpringApplication.runyour #[tokio::main] functionStarting and stopping components is explicit; see the lifecycle.

For Spring modules that have no counterpart yet, see the roadmap.

Beans, injection, and scheduling

In Spring, the container discovers DigestJob, finds beans for its constructor parameters, and starts the scheduled method:

@Component
class DigestJob {
    private final JdbcTemplate jdbc;
    private final Mailer mailer;

    DigestJob(JdbcTemplate jdbc, Mailer mailer) {
        this.jdbc = jdbc;
        this.mailer = mailer;
    }

    @Scheduled(fixedRate = 60 * 60 * 1000)
    void run() {
        // Query `jdbc`, send mail with `mailer`.
    }
}

Things to notice:

  • Registration is explicit. ServiceBuilder::new(..).push(..) plays the role of @Bean methods. Nothing is discovered on the classpath.
  • The constructor is a closure. compose_cron_job hands the closure the builder, and s.provide::<T, _>() looks up each registered dependency by type, as constructor injection would. A missing one is a compile error. A component built from a single dependency can instead implement ServiceCreation and be registered with cron_job::<Job, _>(). Dependencies are cloned out of the registry, so keep them cheap to clone (pools, handles, Arcs).
  • The schedule is a constant. INTERVAL is part of the job's type. AtLeast starts runs at least an hour apart, like fixedRate, but never lets two runs overlap. A zero interval does not build.
  • Shutdown is yours. start() returns a CronRuntime. Calling shutdown().await stops scheduling and lets a run in progress finish; abort().await cancels it instead.

Key differences

Wiring is checked at compile time

Spring resolves beans when the context starts. A missing bean is a NoSuchBeanDefinitionException at startup, or in the first integration test that loads the context.

ServiceBuilder is a type-level list, and finding a dependency is a trait bound. Here, ExpireSessions is a cron job whose Dep is SessionStore, but only a Config was registered:

// `SessionStore` was never registered.
let (_services, cron) = ServiceBuilder::new(Config)
    .cron_job::<ExpireSessions, _>()
    .start();

This does not compile:

error[E0277]: the trait bound `(): ProvideAt<SessionStore, _>` is not satisfied
   --> src/main.rs:38:10
    |
 38 |         .cron_job::<ExpireSessions, _>()
    |          ^^^^^^^^ the trait `ProvideAt<SessionStore, _>` is not implemented for `()`

The builder has no runtime map and no Any. A wiring mistake cannot ship. Service builder explains how the lookup works.

No classpath scanning, proxies, or AOP

In Spring, @Transactional, @Cacheable, and @Async work through proxies that the container generates around your beans. Calling an annotated method from inside the same class bypasses the proxy, and the annotation silently does nothing.

wakuwaku has no proxies. Everything that runs around your code is a value you can see in the types:

  • A transaction is a Transaction value that you process with a database handle.
  • A hook around write queries is a wrapper type, such as AfterWrite<Db<..>, ..>.
  • A cache read is a call to Cache::get.

What you read in the code is what runs.

Read-only is a type, not a hint

@Transactional(readOnly = true) is a hint to the transaction manager and the JDBC driver, applied per method at runtime. In wakuwaku, read-only is the capability of a database handle. Code that holds a Db<PgSource, ReadOnly> can run only queries whose declared effect is a read:

/// Wired to a replica: only queries whose effect `ReadOnly` covers compile.
struct UserStats {
    db: Db<PgSource, ReadOnly>,
}

impl UserStats {
    async fn total(&self) -> Result<i64, sqlx::Error> {
        self.db.process(CountUsers).await
    }
}

Passing a query declared as Write<User> to that handle is a compile error:

error[E0277]: the trait bound `ReadOnly: CanWrite<User>` is not satisfied
  --> src/main.rs:25:8
   |
25 |     db.process(DeleteUser(1)).await
   |        ^^^^^^^ the trait `CanWrite<User>` is not implemented for `ReadOnly`

What the type proves

The capability proves routing: code wired to a read-only handle never sends a query declared as a write. It cannot prove that the SQL inside a query declared as a read does not write. Keep the database's own guard on replicas, such as a hot standby or default_transaction_read_only.

Validation produces a type

Bean Validation checks annotated fields and throws when a constraint fails. Nothing in the type of the validated object records that it was checked. With wakuwaku::proof, a successful check returns Proven<T, P>, and a function that needs valid input asks for it:

record SignUp(@NotBlank String username) {}

@PostMapping("/sign-up")
void signUp(@Valid @RequestBody SignUp request) {
    register(request.username()); // register(String) accepts anything
}

Predicates are async and may do I/O, such as checking that a username is not taken. They compose with And and Or. See Validation.

Testing without a context

A Spring test that needs real beans usually starts an application context with @SpringBootTest. A wakuwaku component is a plain value: construct it with create_service, call process or handle, and check the result. The compiler has already checked the wiring, so unit tests can focus on behavior. Quick start shows a complete example, and Testing covers more patterns.

Next steps

On this page