EventModeling Quickstart Guide
Gettings started with the Sliceworkz EventModeling
This guide will help you get started with the EventModeling library for Java.
EventStore is an opinionated Java-base implementation framework for Event Modeling. It builds upon the EventStore, and provides direct implementation mechanisms for Commands, Readmodels, Automations and Translations.
Quickstart Guide
This guide walks you through building your first application with the Sliceworkz Event Modeling framework. We’ll create a simple banking domain with account management functionality.
Prerequisites
- Java 21 or later
- Maven 3.6 or later
Step 1: Add Maven Dependencies
Sliceworkz Eventmodeling is available in maven central.
All Eventmodeling modules are bundled in a Bill-Of-Material pom file. Add the Eventmodeling BOM to your project pom.xml to manage dependency versions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
<properties>
<sliceworkz.eventmodeling.version>0.1.1</sliceworkz.eventmodeling.version>
</properties>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.sliceworkz</groupId>
<artifactId>sliceworkz-eventmodeling-bom</artifactId>
<version>${sliceworkz.eventmodeling.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
...
Add the Event Modeling implementation and an event storage provider to your pom.xml:
1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<!-- Event Modeling Framework Implementation -->
<dependency>
<groupId>org.sliceworkz</groupId>
<artifactId>sliceworkz-eventmodeling-impl</artifactId>
</dependency>
<!-- In-Memory Event Storage (for development/testing) -->
<dependency>
<groupId>org.sliceworkz</groupId>
<artifactId>sliceworkz-eventstore-infra-inmem</artifactId>
</dependency>
</dependencies>
Note: For production use, replace sliceworkz-eventstore-infra-inmem with sliceworkz-eventstore-infra-postgres for PostgreSQL-based event storage.
Step 2: Define Your Domain Events
Create a domain interface that defines your three event types: domain events (internal), inbound events (received from external systems), and outbound events (published to external systems).
Create BankingDomain.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.example.banking;
import java.time.LocalDate;
import org.sliceworkz.eventmodeling.domain.DomainConcept;
import org.sliceworkz.eventmodeling.domain.DomainConceptId;
public interface BankingDomain {
// Define domain concepts for tagging events
DomainConcept CONCEPT_ACCOUNT = DomainConcept.of("account");
DomainConcept CONCEPT_CUSTOMER = DomainConcept.of("customer");
// Domain Events: Internal state changes (use past-tense names)
sealed interface BankingDomainEvent {
record AccountOpened(
DomainConceptId accountId,
DomainConceptId customerId,
LocalDate date
) implements BankingDomainEvent { }
}
// Inbound Events: Events received from external systems
sealed interface BankingInboundEvent {
record CustomerSuspended(
DomainConceptId customerId
) implements BankingInboundEvent { }
}
// Outbound Events: Events published to external systems
sealed interface BankingOutboundEvent {
record AccountAnnounced(
DomainConceptId id
) implements BankingOutboundEvent { }
}
}
Key concepts:
- Use
sealed interfaceto create type-safe event hierarchies - Use
recordfor immutable event data - Name domain events in past-tense (e.g.,
AccountOpened, notOpenAccount) - Domain concepts are used to tag and query events
Step 3: Create a Bounded Context Interface
The bounded context is the main entry point to your application. Create an interface that extends BoundedContext:
Create BankingBoundedContext.java:
1
2
3
4
5
6
7
8
9
10
11
package com.example.banking;
import org.sliceworkz.eventmodeling.boundedcontext.BoundedContext;
import com.example.banking.BankingDomain.BankingDomainEvent;
import com.example.banking.BankingDomain.BankingInboundEvent;
import com.example.banking.BankingDomain.BankingOutboundEvent;
public interface BankingBoundedContext
extends BoundedContext<BankingDomainEvent, BankingInboundEvent, BankingOutboundEvent> {
// This interface provides execute() and read() capabilities
}
Step 4: Create a STATE_CHANGE Feature Slice (Command)
State change features handle commands that modify state. We’ll implement a feature to open bank accounts.
4.1: Create the Feature Slice Configuration
Create features/openaccount/OpenAccountFeatureSlice.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.banking.features.openaccount;
import org.sliceworkz.eventmodeling.boundedcontext.BoundedContextBuilder;
import org.sliceworkz.eventmodeling.slices.FeatureSlice;
import org.sliceworkz.eventmodeling.slices.FeatureSlice.Type;
import org.sliceworkz.eventmodeling.slices.FeatureSliceConfiguration;
import com.example.banking.BankingBoundedContext;
import com.example.banking.BankingDomain.BankingDomainEvent;
import com.example.banking.BankingDomain.BankingInboundEvent;
import com.example.banking.BankingDomain.BankingOutboundEvent;
@FeatureSlice(
type = Type.STATE_CHANGE,
context = "banking",
chapter = "Account management",
tags = {"online"}
)
public class OpenAccountFeatureSlice
implements FeatureSliceConfiguration<BankingDomainEvent, BankingInboundEvent, BankingOutboundEvent> {
@Override
public void configure(
BoundedContextBuilder<BankingDomainEvent, BankingInboundEvent, BankingOutboundEvent> builder) {
// Commands are automatically discovered, no explicit registration needed
}
}
Key concepts:
@FeatureSliceannotation marks this class for automatic discoverytype = Type.STATE_CHANGEindicates this feature handles commandscontext,chapter, andtagsprovide organizational metadata
4.2: Create the Command Implementation
Create features/openaccount/OpenAccountCommand.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.example.banking.features.openaccount;
import java.time.LocalDate;
import org.sliceworkz.eventmodeling.commands.Command;
import org.sliceworkz.eventmodeling.commands.CommandContext;
import org.sliceworkz.eventmodeling.commands.CommandResult;
import org.sliceworkz.eventmodeling.domain.DomainConceptId;
import org.sliceworkz.eventmodeling.domain.DomainConceptTag;
import org.sliceworkz.eventstore.events.Tags;
import com.example.banking.BankingDomain;
import com.example.banking.BankingDomain.BankingDomainEvent;
import com.example.banking.BankingDomain.BankingDomainEvent.AccountOpened;
public class OpenAccountCommand implements Command<BankingDomainEvent> {
private final DomainConceptId customerId;
public OpenAccountCommand(DomainConceptId customerId) {
this.customerId = customerId;
}
@Override
public CommandResult<BankingDomainEvent, BankingDomainEvent> execute(
CommandContext<BankingDomainEvent, BankingDomainEvent> context) {
var result = context.noDecisionModels();
// Generate a unique account ID
DomainConceptId accountId = DomainConceptId.create();
// Raise the AccountOpened event with appropriate tags
result.raiseEvent(
new AccountOpened(accountId, customerId, LocalDate.now()),
Tags.of(
DomainConceptTag.of(BankingDomain.CONCEPT_ACCOUNT, accountId),
DomainConceptTag.of(BankingDomain.CONCEPT_CUSTOMER, customerId)
)
);
return result;
}
}
Key concepts:
- Commands implement
Command<EVENT_TYPE> execute()method receives aCommandContextand returns aCommandResult- Use
result.raiseEvent()to append events to the event store - Tag events with domain concepts for efficient querying
Step 5: Create a STATE_READ Feature Slice (Read Model)
Read models project events into queryable views. We’ll create a read model to view account details.
5.1: Create the Feature Slice Configuration
Create features/accountdetails/AccountDetailsFeatureSlice.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.example.banking.features.accountdetails;
import org.sliceworkz.eventmodeling.boundedcontext.BoundedContextBuilder;
import org.sliceworkz.eventmodeling.slices.FeatureSlice;
import org.sliceworkz.eventmodeling.slices.FeatureSlice.Type;
import org.sliceworkz.eventmodeling.slices.FeatureSliceConfiguration;
import com.example.banking.BankingDomain.BankingDomainEvent;
import com.example.banking.BankingDomain.BankingInboundEvent;
import com.example.banking.BankingDomain.BankingOutboundEvent;
@FeatureSlice(
type = Type.STATE_READ,
context = "banking",
tags = {"online"}
)
public class AccountDetailsFeatureSlice
implements FeatureSliceConfiguration<BankingDomainEvent, BankingInboundEvent, BankingOutboundEvent> {
@Override
public void configure(
BoundedContextBuilder<BankingDomainEvent, BankingInboundEvent, BankingOutboundEvent> builder) {
// Register the read model
builder.readmodel(AccountDetailsReadModel.class);
}
}
5.2: Create the Read Model Implementation
Create features/accountdetails/AccountDetailsReadModel.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.example.banking.features.accountdetails;
import java.time.LocalDate;
import java.util.Optional;
import org.sliceworkz.eventmodeling.domain.DomainConceptId;
import org.sliceworkz.eventmodeling.domain.DomainConceptTags;
import org.sliceworkz.eventmodeling.readmodels.ReadModel;
import org.sliceworkz.eventstore.query.EventQuery;
import org.sliceworkz.eventstore.query.EventTypesFilter;
import com.example.banking.BankingDomain;
import com.example.banking.BankingDomain.BankingDomainEvent;
import com.example.banking.BankingDomain.BankingDomainEvent.AccountOpened;
public class AccountDetailsReadModel implements ReadModel<BankingDomainEvent> {
private final DomainConceptId accountId;
private AccountDetails account;
public AccountDetailsReadModel(DomainConceptId accountId) {
this.accountId = accountId;
}
public Optional<AccountDetails> getAccountDetails() {
return Optional.ofNullable(account);
}
@Override
public EventQuery eventQuery() {
// Query only events tagged with this specific account
return EventQuery.forEvents(
EventTypesFilter.any(),
DomainConceptTags.of(BankingDomain.CONCEPT_ACCOUNT, accountId)
);
}
@Override
public void when(BankingDomainEvent event) {
// Handle events to build up the read model state
switch (event) {
case AccountOpened ao:
this.account = AccountDetails.of(
ao.accountId().value(),
ao.customerId().value(),
ao.date()
);
break;
}
}
public record AccountDetails(String accountId, String customerId, LocalDate openDate) {
public AccountDetails {
if (accountId == null) throw new IllegalArgumentException("accountId is required");
if (customerId == null) throw new IllegalArgumentException("customerId is required");
if (openDate == null) throw new IllegalArgumentException("openDate is required");
}
public static AccountDetails of(String accountId, String customerId, LocalDate openDate) {
return new AccountDetails(accountId, customerId, openDate);
}
}
}
Key concepts:
- Read models implement
ReadModel<EVENT_TYPE> eventQuery()defines which events to replay into this modelwhen()method handles each event to build up the model’s state- Read models are instantiated per query (pass parameters via constructor)
Step 6: Build and Initialize the Bounded Context
Now tie everything together by creating and configuring the bounded context:
Create BankingApplication.java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package com.example.banking;
import java.util.Optional;
import org.sliceworkz.eventmodeling.boundedcontext.BoundedContext;
import org.sliceworkz.eventmodeling.domain.DomainConceptId;
import org.sliceworkz.eventmodeling.events.Instance;
import org.sliceworkz.eventmodeling.events.InstanceFactory;
import org.sliceworkz.eventstore.EventStore;
import org.sliceworkz.eventstore.EventStoreFactory;
import org.sliceworkz.eventstore.events.Event;
import org.sliceworkz.eventstore.events.EventReference;
import org.sliceworkz.eventstore.infra.inmem.InMemoryEventStorage;
import org.sliceworkz.eventstore.spi.EventStorage;
import com.example.banking.BankingDomain.BankingDomainEvent;
import com.example.banking.BankingDomain.BankingDomainEvent.AccountOpened;
import com.example.banking.BankingDomain.BankingInboundEvent;
import com.example.banking.BankingDomain.BankingOutboundEvent;
import com.example.banking.features.accountdetails.AccountDetailsReadModel;
import com.example.banking.features.openaccount.OpenAccountCommand;
public class BankingApplication {
public static void main(String[] args) {
// 1. Create event storage (in-memory for this example)
EventStorage eventStorage = InMemoryEventStorage.newBuilder().build();
EventStore eventStore = EventStoreFactory.get().eventStore(eventStorage);
// 2. Create an instance identifier (for multi-tenancy/deployment)
Instance instance = InstanceFactory.determine("banking-app");
// 3. Build the bounded context
BankingBoundedContext bc = BoundedContext.newBuilder(
BankingDomainEvent.class,
BankingInboundEvent.class,
BankingOutboundEvent.class
)
.name("banking")
.eventStorage(eventStorage)
.instance(instance)
.rootPackage(BankingApplication.class.getPackage()) // Scans for @FeatureSlice
.build(BankingBoundedContext.class);
// 4. Execute a command
DomainConceptId customerId = DomainConceptId.create();
Optional<EventReference> eventRef = bc.execute(new OpenAccountCommand(customerId));
if (eventRef.isPresent()) {
System.out.println("Account opened successfully!");
// 5. Retrieve the event that was raised
var eventStream = eventStore.getEventStream(
org.sliceworkz.eventstore.stream.EventStreamId
.forContext("banking")
.withPurpose("domain"),
BankingDomainEvent.class
);
AccountOpened accountOpened = eventStream
.getEventById(eventRef.get().id())
.map(Event::data)
.map(e -> (AccountOpened) e)
.get();
// 6. Query the read model
AccountDetailsReadModel readModel = bc.read(
AccountDetailsReadModel.class,
accountOpened.accountId()
);
readModel.getAccountDetails().ifPresent(details -> {
System.out.println("Account Details:");
System.out.println(" Account ID: " + details.accountId());
System.out.println(" Customer ID: " + details.customerId());
System.out.println(" Open Date: " + details.openDate());
});
}
}
}
Step 7: Run Your Application
Build and run your application:
1
mvn clean compile exec:java -Dexec.mainClass="com.example.banking.BankingApplication"
You should see output similar to:
1
2
3
4
5
Account opened successfully!
Account Details:
Account ID: <generated-uuid>
Customer ID: <generated-uuid>
Open Date: 2025-11-21
Project Structure
Your final project structure should look like this:
1
2
3
4
5
6
7
8
9
10
11
src/main/java/com/example/banking/
├── BankingDomain.java # Event definitions
├── BankingBoundedContext.java # Bounded context interface
├── BankingApplication.java # Main application
└── features/
├── openaccount/
│ ├── OpenAccountFeatureSlice.java # Feature configuration
│ └── OpenAccountCommand.java # Command implementation
└── accountdetails/
├── AccountDetailsFeatureSlice.java # Feature configuration
└── AccountDetailsReadModel.java # Read model implementation
Next Steps
Now that you have a working Event Modeling application, you can:
- Add more events to
BankingDomainEvent(e.g.,MoneyDeposited,MoneyWithdrawn) - Implement decision models to enforce business rules in commands
- Create automations to implement automated activities
- Add translators to handle inbound events from external systems
- Implement dispatchers for the outbox pattern to publish outbound events
- Switch to PostgreSQL for production-ready event storage
- Add tests using the
sliceworkz-eventmodeling-testingmodule
For more examples, see the sliceworkz-eventmodeling-examples module in the repository.