The name
Verba volant, scripta manent.
That is the whole idea behind event sourcing. In Scripta, every event is written once and kept forever, and the current state of your domain is simply what those events add up to.
What it looks like
An aggregate is a plain Java class. Events are records behind a sealed interface, so the compiler checks that every event is handled.
public sealed interface AccountEvent {
record AccountOpened(String owner) implements AccountEvent {}
record MoneyDeposited(BigDecimal amount) implements AccountEvent {}
record MoneyWithdrawn(BigDecimal amount) implements AccountEvent {}
}
public final class BankAccount extends Aggregate<AccountEvent> {
private BigDecimal balance = BigDecimal.ZERO;
public void deposit(BigDecimal amount) {
if (amount.signum() <= 0) throw new IllegalArgumentException("Amount must be positive");
raise(new MoneyDeposited(amount));
}
public void withdraw(BigDecimal amount) {
if (balance.compareTo(amount) < 0) throw new IllegalStateException("Insufficient funds");
raise(new MoneyWithdrawn(amount));
}
@Override
protected void apply(AccountEvent event) {
switch (event) {
case AccountOpened e -> balance = BigDecimal.ZERO;
case MoneyDeposited e -> balance = balance.add(e.amount());
case MoneyWithdrawn e -> balance = balance.subtract(e.amount());
}
}
}
@Test
void withdrawingRaisesMoneyWithdrawn() {
given(new AccountOpened("Ada"), new MoneyDeposited(new BigDecimal("100")))
.when(account -> account.withdraw(new BigDecimal("30")))
.then(new MoneyWithdrawn(new BigDecimal("30")));
}
Scripta is pre-release, so the API shown here is a design target and may still change.
Principles
-
Zero runtime dependencies
The core module depends on the JDK and nothing else. Integrations live in separate, optional modules.
-
Plain Java, no magic
Aggregates and events are ordinary classes, records and sealed types. No reflection tricks, no bytecode generation.
-
Optimistic concurrency
Every append checks the expected stream version, so conflicting writes fail loudly instead of silently overwriting.
-
Projections you can trust
Read models track checkpoints and can be rebuilt from the event log whenever their shape changes.
-
Testing first
Given/when/then fixtures let you test behaviour as events in, events out, without a database.
-
Pluggable storage
The event store sits behind a small interface. PostgreSQL is the first implementation.
Quickstart
Scripta is in early development and is not yet published to Maven Central. The coordinates below are the ones it will use. Until a release is out, build the project from source with mvn install to use the snapshot locally.
<dependency>
<groupId>dev.scriptaframework</groupId>
<artifactId>scripta-core</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
Requires Java 21 or newer.
Roadmap
Nothing here is finished yet. This is the order things are being built in.
- Core: aggregates, events, repositories, test fixtures In progress
- PostgreSQL event store Planned
- Jackson serialization Planned
- Spring Boot starter Planned