Class EventSourcedEntity<S,E>

Object
akka.javasdk.eventsourcedentity.EventSourcedEntity<S,E>
Type Parameters:
S - The type of the state for this entity
E - The parent type of the event hierarchy for this entity, required to be a sealed interface
Direct Known Subclasses:
PromptTemplate, SessionMemoryEntity

public abstract class EventSourcedEntity<S,E> extends Object
Event Sourced Entities are stateful components that persist changes as events in a journal rather than storing the current state directly. The current entity state is derived by replaying all persisted events. This approach provides a complete audit trail and enables reliable state replication.

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

  1. Model the entity's state and its domain events
  2. 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 an EventSourcedEntity.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:

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. Use EventSourcedEntity.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.

  • Constructor Details

    • EventSourcedEntity

      public EventSourcedEntity()
  • Method Details

    • emptyState

      public S 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

      protected final CommandContext 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

      protected final EventContext eventContext()
      Provides access to additional context and metadata when handling an event in the applyEvent(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 the applyEvent(E) method
    • applyEvent

      public abstract S applyEvent(E event)
      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.
      It's important to keep the event handler side effect free. This means that it should only apply the event on the current state and return the updated state. This is because the event handler is called during recovery.

      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

      protected final S 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 using effects().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

      protected final EventSourcedEntity.Effect.Builder<S,E> effects()