Class EventSourcedEntity<S,E>
- Type Parameters:
S
- The type of the state for this entityE
- The parent type of the event hierarchy for this entity, required to be a sealed interface
- Direct Known Subclasses:
PromptTemplate
,SessionMemoryEntity
Event Sourced Entities provide strong consistency guarantees through entity sharding, where each entity instance is identified by a unique id and distributed across the service cluster. Only one instance of each entity exists in the cluster at any time, ensuring sequential message processing without concurrency concerns.
The entity state is kept in memory while active and can serve read requests or command validation without additional reads from the journal. Inactive entities are passivated and recover their state by replaying events from the journal when accessed again.
Implementation Steps
- Model the entity's state and its domain events
- Implement behavior in command and event handlers
Event Sourcing Model
Unlike traditional CRUD systems, Event Sourced Entities never update state directly. Instead:
- Commands validate business rules and persist events representing state changes
- Events are applied to update the entity state through the
applyEvent(E)
method - The current state is always derived from the complete sequence of events (and snapshot, if exists)
Command Handlers
Command handlers are methods that return anEventSourcedEntity.Effect
and define how the entity responds to commands.
The Effect API allows you to:
- Persist events and send a reply to the caller
- Reply directly without persisting events
- Return an error message
- Delete the entity
Event Handlers
Events must inherit from a common sealed interface, and the applyEvent(E)
method should
be implemented using a switch statement for compile-time completeness checking.
Snapshots
Akka automatically creates snapshots as an optimization to avoid replaying all events during entity recovery. Snapshots are created after a configurable number of events and are handled transparently without requiring specific code.
Example Implementation
@ComponentId("shopping-cart")
public class ShoppingCartEntity extends EventSourcedEntity<ShoppingCart, ShoppingCartEvent> {
private final String entityId;
public ShoppingCartEntity(EventSourcedEntityContext context) {
this.entityId = context.entityId();
}
@Override
public ShoppingCart emptyState() {
return new ShoppingCart(entityId, Collections.emptyList(), false);
}
public Effect<Done> addItem(LineItem item) {
var event = new ShoppingCartEvent.ItemAdded(item);
return effects()
.persist(event)
.thenReply(newState -> Done.getInstance());
}
public ReadOnlyEffect<ShoppingCart> getCart() {
return effects().reply(currentState());
}
@Override
public ShoppingCart applyEvent(ShoppingCartEvent event) {
return switch (event) {
case ShoppingCartEvent.ItemAdded evt -> currentState().addItem(evt.item());
// handle all events ...
};
}
}
Concrete classes can accept the following types in the constructor:
EventSourcedEntityContext
- provides entity context information- Custom types provided by a
DependencyProvider
from the service setup
Concrete classes must be annotated with ComponentId
with a
stable, unique identifier that cannot be changed after production deployment.
Multi-region Replication
Event Sourced Entities support multi-region replication for resilience and performance. Write requests are handled by the primary region, while read requests can be served from any region. UseEventSourcedEntity.ReadOnlyEffect
for read-only operations that can be served from replicas.
Immutable state record
It is recommended to use immutable state objects, such as Java records, for the entity state. Immutable state ensures thread safety and prevents accidental modifications that could lead to inconsistent state or concurrency issues.
While mutable state classes are supported, they require careful handling:
- Mutable state should not be shared outside the entity
- Mutable state should not be passed to other threads, such as in
CompletionStage
operations - Any modifications to mutable state must be done within the entity's event handler
Collections in State: Collections (such as List
, Set
, Map
) are
typically mutable even when contained within immutable objects. When updating state that contains collections,
you should create copies of the collections rather than modifying them in place. This ensures that the
previous state remains unchanged and prevents unintended side effects.
Performance Considerations: Defensive copying of collections can introduce performance overhead, especially for large collections or frequent updates. In performance-critical scenarios, this recommendation can be carefully tuned by using mutable state with strict adherence to the safety guidelines mentioned above.
Using immutable records with defensive copying of collections eliminates concurrency concerns and is the preferred approach for state modeling in most cases.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic interface
An Effect describes the actions that the Akka runtime should perform after a command handler completes.static interface
An effect that is known to be read only and does not update the state of the entity. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionabstract S
applyEvent
(E event) This is the main event handler method.protected final CommandContext
Provides access to additional context and metadata for the current command being processed.protected final S
Returns the current state of this entity as derived from all persisted events.protected final EventSourcedEntity.Effect.Builder
<S, E> effects()
Returns the initial empty state object for this entity.protected final EventContext
Provides access to additional context and metadata when handling an event in theapplyEvent(E)
method.protected boolean
Returns whether this entity has been marked for deletion.
-
Constructor Details
-
EventSourcedEntity
public EventSourcedEntity()
-
-
Method Details
-
emptyState
Returns the initial empty state object for this entity. This state is used when the entity is first created and before any events have been persisted and applied.Also known as "zero state" or "neutral state". This method is called when the entity is instantiated for the first time or when recovering from the journal without any persisted events.
The default implementation returns
null
. Override this method to provide a more meaningful initial state for your entity.- Returns:
- the initial state object, or
null
if no initial state is needed
-
commandContext
Provides access to additional context and metadata for the current command being processed. This includes information such as the command name, entity id, sequence number, and tracing context.This method can only be called from within a command handler method. Attempting to access it from the constructor or inside the
applyEvent(E)
method will result in an exception.- Returns:
- the command context for the current command
- Throws:
IllegalStateException
- if accessed outside a command handler method
-
eventContext
Provides access to additional context and metadata when handling an event in theapplyEvent(E)
method. This includes information such as the sequence number of the event being processed.This method can only be called from within the
applyEvent(E)
method. Attempting to access it from the constructor or command handler will result in an exception.- Returns:
- the event context for the current event being processed
- Throws:
IllegalStateException
- if accessed outside theapplyEvent(E)
method
-
applyEvent
This is the main event handler method. Whenever an event is persisted, this handler will be called. It should return the new state of the entity.Note that this method is called in two situations:
- when one or more events are persisted by the command handler, this method is called to produce the new state of the entity.
- when instantiating an entity from the event journal, this method is called to restore the state of the entity.
Events are required to inherit from a common sealed interface, and it's recommend to implement this method using a switch statement. As such, the compiler can check if all existing events are being handled.
// example of sealed event interface with concrete events implementing it public sealed interface Event { @TypeName("created") public record UserCreated(String name, String email) implements Event {}; @TypeName("email-updated") public record EmailUpdated(String newEmail) implements Event {}; } // example of applyEvent implementation public User applyEvent(Event event) { return switch (event) { case UserCreated userCreated -> new User(userCreated.name, userCreated.email); case EmailUpdated emailUpdated -> this.copy(email = emailUpdated.newEmail); } }
-
currentState
Returns the current state of this entity as derived from all persisted events. This represents the latest state after applying all events in the journal.Important: Modifying the returned state object directly will not persist the changes. State can only be updated by persisting events through command handlers, which are then applied via the
applyEvent(E)
method.This method can only be called from within a command handler or event handler method. Attempting to access it from the constructor or outside of command/event processing will result in an exception.
- Returns:
- the current state of the entity, which may be
null
if no initial state is defined - Throws:
IllegalStateException
- if accessed outside a handler method
-
isDeleted
protected boolean isDeleted()Returns whether this entity has been marked for deletion. When an entity is deleted usingeffects().persist(finalEvent).deleteEntity()
, it will still exist for some time before being completely removed.After deletion, the entity can still handle read requests but no further events can be persisted. The entity and its events will be completely cleaned up after a default period of one week to allow downstream consumers time to process all events.
- Returns:
true
if the entity has been deleted,false
otherwise
-
effects
-