
Building with Akka: Customer Registry App (part 2 – Domain Model and Entity)

Welcome to the next part of our “Building with Akka” series, where we implement the core logic of our Customer Registry Application. This article builds on our previous introduction, demonstrating how to model customer data, define events for state changes, and implement commands that drive the customer lifecycle. Follow along as we create a robust, event-sourced system that showcases the elegant architecture patterns made possible by the Akka SDK. This is part of a series of articles and in case you missed the previous one, we recommend starting with this introduction.
To begin implementing our system, we need to model the customer in three complementary ways. First, we’ll create a domain model class that represents the customer’s structure and properties. We’ll also define events that capture state changes, such as when customers are created or updated. Finally, we’ll implement commands that trigger these changes and drive the customer lifecycle. This layered approach separates our customer’s data model from the actions that modify it and the events that track its history.
Domain model
Let’s start by introducing the domain class Customer:
package customer.domain;
public record Customer(String email, String name, Address address) {
public Customer withName(String newName) { // <1>
return new Customer(email, newName, address);
}
public Customer withAddress(Address newAddress) { // <2>
return new Customer(email, name, newAddress);
}
}
public record Address(String street, String city) {}A Customer is an immutable Java record, and this implies that to change the customer’s name or address, a new instance must be created. Indeed, the methods withName and withAddress returns a new customer.
Events
The events are defined as a class hierarchy of type CustomerEvent:
package customer.domain;
import akka.javasdk.annotations.Migration;
import akka.javasdk.annotations.TypeName;
public sealed interface CustomerEvent {
@TypeName("internal-customer-created") // <1>
@Migration(CustomerCreatedMigration.class)
record CustomerCreated(String email, String name, Address address) implements CustomerEvent {}
@TypeName("internal-name-changed")
record NameChanged(String newName) implements CustomerEvent {}
@TypeName("internal-address-changed")
record AddressChanged(Address address) implements CustomerEvent {}
}There are three possible events:
- a customer is added to the registry:
CustomerCreated - a customer name changed:
NameChanged - a customer address changed:
AddressChanged
The annotation @TypeName defines a logical type name for the events, otherwise the fully qualified class name is used, limiting refactoring and maintainability. Logical names are recommended. The annotation @Migration is used to support versioning of events. The class CustomerCreatedMigration is used to allow the deserialization of different versions of the CustomerCreated event. These details are out-of-scope for us, but you can find more of them in the API documentation.
Entity
The class CustomerEntity is an Event Sourced state model that captures changes to data by storing events in a journal. The current state of the entity is derived from the persisted events. When implementing an Event Sourced Entity, you first define what will be its internal state S (e.g. the Customer model class), the commands it will handle (we will define the methods later, such as create, changeName, etc.) and the events E it will persist to modify its state (e.g. the CustomerEvent classes).
Let’s start by looking at the entity class:
package customer.application;
import akka.javasdk.annotations.ComponentId;
import akka.javasdk.eventsourcedentity.EventSourcedEntity;
import customer.domain.Customer;
import customer.domain.CustomerEvent;
// other imports omitted
@ComponentId("customer")
public class CustomerEntity extends EventSourcedEntity<Customer, CustomerEvent> {
// ...
}The class extends a EventSourcedEntity<S, E> and it’s annotated with @ComponentId to assign a type identifier.
The identifier (required for all component types aside from Endpoints) is common for all instances of this entity but must be stable – cannot be changed after a production deploy – and unique across the different entity types in the service. A different identifier means a different representation in storage, so changing this identifier will create a new class of component and all previous instances using the old identifier won’t be accessible anymore. For more information, see Identifying the Entity.
Event Handler
The first part we define is the events’ handler. Handling an event means defining how the current state of the entity must change when a given event occurred.
In Akka, the definition of the event handler is provided by implementing the abstract method S applyEvent(E). For example:
@Override
public Customer applyEvent(CustomerEvent event) {
return switch (event) {
case CustomerCreated created ->
new Customer(created.email(), created.name(), created.address());
case NameChanged nameChanged ->
currentState().withName(nameChanged.newName());
case AddressChanged addressChanged ->
currentState().withAddress(addressChanged.address());
};
}The currentState() function allows to access the current state of the entity, i.e. a Customer object. To create a new state, we leverage the methods withName and withAddress, which return a new customer instance.
Commands
Commands are handled by – surprise, surprise – command handlers. A command handler is simply a method returning an Effect or a ReadOnlyEffect. When handling a command, you use the Effect API to:
- persist events and build a reply;
- directly returning to the caller if the command is not requesting any state change;
- rejected the command by returning an error;
- instruct the runtime to delete the entity.
An Effect is a description of what the runtime needs to do after the command is handled. You can think of it as a set of instructions you are passing to the runtime, which will process the instructions on your behalf.
The set of commands of our application are:
- create a customer;
- get the customer data;
- update the customer name;
- update the customer address.
The create method uses the events() facility to create an event builder, then invoke persist passing the CustomerCreated event. Finally, an ack message Done is provided to the caller.
public Effect<Done> create(Customer customer) {
logger.info("Creating {}", customer);
return effects()
.persist(new CustomerCreated(
customer.email(),
customer.name(),
customer.address()))
.thenReply(__ -> done());
}Note that the state is not updated. The update is handle automatically by the runtime system, which will invoke the applyEvent method that we previously defined.
Another command is defined by the getCustomer method, which returns the current state of the customer entity. The current state is checked: if undefined, the entity creates an error reply which can be handled by the caller. Otherwise, the current state is provided via the reply method. Note the usage of ReadOnlyEffect because this command does not mutate the state.
public ReadOnlyEffect<Customer> getCustomer() {
if (currentState() == null)
return effects().error(
"No customer found for id '" + commandContext().entityId() + "'"
);
else
return effects().reply(currentState());
}Since the check on the current state is needed by other commands too, we can externalize its implementation in a new method:
private <T> ReadOnlyEffect<T> errorNotFound() {
return effects().error(
"No customer found for id '" + commandContext().entityId() + "'"
);
}Finally, we complete our set of commands as follows:
public Effect<Done> changeName(String newName) {
if (currentState() == null)
return errorNotFound();
else
return effects()
.persist(new NameChanged(newName))
.thenReply(__ -> done());
}
public Effect<Done> changeAddress(Address newAddress) {
if (currentState() == null)
return errorNotFound();
else
return effects()
.persist(new AddressChanged(newAddress))
.thenReply(__ -> done());
}Awesome! We completed the implementation of the customer entity class. In particular:
- we defined the domain model classes to represent the customer state;
- we implemented the event handler, used to mutate the state;
- we defined the commands as class methods, which produce and persist events through effects.
In the next part, we will explore the implementation of the API layer, which enables interaction with a customer entity. We’ll also define custom views to query the entire dataset of customers.



