Skip to content

Commit 2d2662f

Browse files
committed
update documentation
1 parent 8c07509 commit 2d2662f

File tree

19 files changed

+787
-10
lines changed

19 files changed

+787
-10
lines changed

README.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,145 @@ a CQRS/ES framework in scala using akka persistence
33

44
![](docs/diagrams/src/CQRS.svg)
55

6+
"CQRS is simply the creation of two objects where there was previously only one. The separation occurs based upon whether the methods are a command or a query (the same definition that is used by Meyer in Command and Query Separation: a command is any method that mutates state and a query is any method that returns a value)."
7+
—Greg Young, CQRS, Task Based UIs, Event Sourcing agh!
8+
9+
## Write Side
10+
11+
### Akka Persistence
12+
13+
**generic-persistence-api** relies on [Akka Persistence](https://doc.akka.io/docs/akka/current/typed/persistence.html) which provides through **Cluster Sharding** and **EventSourcedBehavior** a **scalable** and **resilient** way to implement the write-side of CQRS using commands and events that simplify the implementation of event-sourced systems.
14+
15+
#### Cluster Sharding
16+
17+
Akka Persistence provides a powerful mechanism for scaling out stateful actors using cluster sharding. Cluster sharding allows stateful actors to be distributed across a cluster of nodes, while ensuring that messages are routed to the correct actor instance regardless of which node it is running on. This can help improve the scalability and reliability of distributed systems built using Akka Persistence.
18+
19+
Here's an overview of how cluster sharding works within Akka Persistence:
20+
21+
1. **Actor entity**: Each stateful actor that needs to be distributed using cluster sharding is referred to as an "actor entity".
22+
2. **Sharding region**: A "sharding region" is responsible for managing a set of actor entities. Each sharding region is responsible for a subset of the actor entities in the system.
23+
3. **Shard identifier**: Each actor entity is assigned a unique "shard identifier" that is used to determine which sharding region is responsible for managing that entity.
24+
4. **Message routing**: When a message is sent to an actor entity, the message is first sent to the sharding region responsible for managing that entity. The sharding region then routes the message to the appropriate actor instance based on its shard identifier.
25+
5. **Node awareness**: The sharding mechanism is aware of the state of the cluster, so it can ensure that actor instances are running on nodes that are currently available and healthy.
26+
6. **Persistence**: Akka Persistence is used to persist the state of each actor entity, ensuring that the state is durable and can survive node failures and other types of system failures.
27+
28+
In the context of Akka's Cluster Sharding, the **ask pattern** can be used to send a message to an entity actor running on a remote node and receive a response back. Here's an overview of how the ask pattern works in the context of Cluster Sharding:
29+
30+
1. **Send a message**: An actor sends a message to an entity actor using its EntityRef.
31+
2. **ask the entity**: The sender actor can call the ask method on the EntityRef to send the message and expect a response.
32+
3. **Proxy actor**: When the ask method is called, the EntityRef creates a "proxy" actor to forward the message to the remote entity actor.
33+
4. **Message routing**: The message is routed to the sharding coordinator and then to the correct node where the entity actor is running based on the entity's shard identifier.
34+
5. **Message processing**: The entity actor receives the message and processes it, potentially modifying its state and sending back a response message.
35+
6. **Future completion**: When the response message is received, the Future representing the response message is completed with the received response message.
36+
7. **Timeout handling**: If the response message is not received within the specified timeout, the Future will be completed with a TimeoutException.
37+
38+
#### EventSourcedBehavior
39+
40+
The write-side using Akka **EventSourcedBehavior** typically involves the following steps:
41+
42+
1. **Command handling**: When a **command** is received, it is typically handled by an instance of the EventSourcedBehavior actor that represents the aggregate root for the corresponding domain entity. The actor uses the current **state** of the entity to determine how to handle the command, and may generate one or more **events** before eventually responding to the sender (**ask pattern**).
43+
2. **Event persistence**: Once the events have been generated, they are appended to a **journal** (an event log using Akka Persistence's event sourcing mechanism), with additional **tags**. This allows events to be easily filtered and queried based on their tags, improving the efficiency of read-side **projections**.
44+
3. **Event replay**: When the actor is created or restarted, Akka Persistence automatically replays all the events from the event log, allowing the actor to rebuild its current state based on the events.
45+
4. **Snapshotting**: To avoid replaying all events every time the actor is created or restarted, Akka EventSourcedBehavior supports snapshotting. Every N events or when a given predicate of the state is fulfilled, the actor can save a **snapshot** of its current state, including any accumulated events and their tags after the previous snapshot. This snapshot is persisted alongside the event log and can be used to restore the state of the actor at a later point in time, rather than replaying all events from the beginning.
46+
47+
The framework handles the complexities of event persistence, replay, and snapshotting, allowing developers to focus on defining the behavior of the domain entity in response to commands.
48+
49+
The resulting system is highly efficient, with the ability to quickly rebuild its state from a **snapshot** and replay only a subset of events, while still ensuring the accuracy of the system's state.
50+
51+
Moreover, providing the ability to **tag events** enables **read-side projections** to be easily implemented and maintained, improving the overall performance and scalability of the system.
52+
53+
### Event Sourcing with generic-persistence-api
54+
55+
![](docs/diagrams/out/EventSourcing.svg)
56+
57+
#### Commands
58+
59+
In CQRS, commands play a critical role in defining the write-side of the system. A command is a message that encapsulates a user's intent to perform a specific action, such as creating a new account, updating an existing record, or deleting data. The main purpose of commands is to initiate a change in the state of the system.
60+
61+
Command responses play also an important role in providing feedback to the user who initiated the command. When a user sends a command to the system, they expect a response that confirms that the command has been successfully executed or that an error has occurred.
62+
63+
The framework defines the trait **_Command_** as the root interface of any command sends to a recipient in the system, and the trait **_CommandResult_** as the root interface of any command response.
64+
65+
![](docs/diagrams/out/Command.svg)
66+
67+
#### Events
68+
69+
Events provide the basis for synchronizing the changes on the write-side (that result from processing commands) with the read side.
70+
71+
If the write-side raises an event whenever the state of the application changes, the read side should respond to that event and update the data that is used by its queries and views.
72+
73+
The framework defines the trait **_Event_** as the root interface of any event in the system.
74+
75+
![](docs/diagrams/out/Event.svg)
76+
77+
#### State
78+
79+
The state of an entity actor can be divided into two parts: the in-memory state and the persistent state.
80+
81+
The in-memory state represents the current state of the actor as it is being processed. This state is modified as the actor receives commands and generates events in response. The in-memory state can be any type of data structure, such as a case class or a collection.
82+
83+
The persistent state represents the state of the actor as it has been stored in the journal. This state includes all events that have been generated by the actor, and can be used to rebuild the in-memory state of the actor in case of a failure or restart. The persistent state is maintained by the Akka Persistence journal and is not directly accessible by the actor.
84+
85+
When an entity actor is created, it starts with an empty in-memory state and persistent state. As events are persisted to the journal, the persistent state is updated, and the in-memory state is rebuilt by replaying the events from the journal.
86+
87+
The framework defines the trait **_State_** as the root interface of the in-memory state of any entity actor.
88+
89+
![](docs/diagrams/out/State.svg)
90+
91+
#### Serialization
92+
93+
Akka Persistence provides a built-in serialization mechanism that uses the Akka Serialization library. This library allows you to define serializers for your custom data types, so that they can be serialized and deserialized automatically when they are persisted to the event store.
94+
95+
When an Akka actor receives a command, it creates one or more domain events and sends them to the event journal for persistence. Before the events are persisted, they are serialized using the configured serialization mechanism. When events are read from the event journal, they are deserialized back into domain events using the same mechanism.
96+
97+
Finally, before persisting a snapshot, the current state of the actor (its in-memory state) is serialized using the configured serialization mechanism.
98+
99+
The framework supports natively three types of **Serializers**:
100+
+ **proto**
101+
+ **jackson-cbor**
102+
+ **chill**
103+
104+
```hocon
105+
akka {
106+
actor {
107+
allow-java-serialization = off
108+
109+
serializers {
110+
proto = "akka.remote.serialization.ProtobufSerializer"
111+
jackson-cbor = "akka.serialization.jackson.JacksonCborSerializer"
112+
chill = "com.twitter.chill.akka.AkkaSerializer"
113+
}
114+
...
115+
}
116+
}
117+
```
118+
119+
The default [configuration](core/src/main/resources/softnetwork-persistence.conf) defines the following serialization bindings :
120+
121+
```hocon
122+
akka {
123+
actor {
124+
...
125+
enable-additional-serialization-bindings = on
126+
127+
serialization-bindings {
128+
"app.softnetwork.persistence.model.package$Timestamped" = proto
129+
"app.softnetwork.persistence.message.package$ProtobufEvent" = proto # protobuf events
130+
"app.softnetwork.persistence.model.package$ProtobufDomainObject" = proto # protobuf domain objects
131+
"app.softnetwork.persistence.model.package$ProtobufStateObject" = proto # protobuf state objects
132+
133+
"app.softnetwork.persistence.message.package$CborEvent" = jackson-cbor # cbor events
134+
"app.softnetwork.persistence.model.package$CborDomainObject" = jackson-cbor # cbor domain objects
135+
136+
"app.softnetwork.persistence.message.package$Command" = chill
137+
"app.softnetwork.persistence.message.package$CommandResult" = chill
138+
"app.softnetwork.persistence.message.package$Event" = chill
139+
"app.softnetwork.persistence.model.package$State" = chill
140+
141+
}
142+
}
143+
}
144+
```
145+
146+
![](docs/diagrams/out/Serialization.svg)
147+

common/src/main/scala/app/softnetwork/time/package.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ package object time {
2222
Instant.ofEpochSecond(epochSecond)
2323
}
2424

25+
implicit def dateToInstant(date: Date): Instant = {
26+
date.toInstant
27+
}
28+
2529
implicit def instantToDate(instant: Instant): Date = {
2630
Date.from(instant)
2731
}

core/src/main/scala/app/softnetwork/persistence/message/package.scala

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ package object message {
1313
/** Command objects * */
1414
trait Command
1515

16-
/** a command which includes a reference to the actor identity to whom a reply has to be sent
16+
/**
17+
* a command which includes a reference to the actor identity to whom a response has to be sent
1718
*
1819
* @tparam R
1920
* - type of command result
@@ -22,8 +23,8 @@ package object message {
2223
def replyTo: ActorRef[R]
2324
}
2425

25-
/** a wrapper arround a command and its reference to the actor identity to whom a reply has to be
26-
* sent
26+
/**
27+
* a wrapper around a command and its reference to the actor identity to whom a response has to be sent
2728
*
2829
* @tparam C
2930
* - type of command
@@ -61,6 +62,9 @@ package object message {
6162
/** Event objects * */
6263
trait Event
6364

65+
/**
66+
* A particular event that is intended to be broadcast
67+
*/
6468
trait BroadcastEvent extends Event {
6569
def externalUuid: String
6670
}
@@ -114,9 +118,13 @@ package object message {
114118
@SerialVersionUID(0L)
115119
abstract class CountResult(results: Seq[CountResponse]) extends CommandResult
116120

117-
/** Protobuf events * */
121+
/** Protobuf events
122+
* Marker trait for serializing an event with Protobuf
123+
*/
118124
trait ProtobufEvent extends Event
119125

120-
/** Cbor events * */
126+
/** Cbor events
127+
* Marker trait for serializing an event with Jackson CBOR
128+
*/
121129
trait CborEvent extends Event
122130
}

core/src/main/scala/app/softnetwork/persistence/model/package.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ package object model {
1010
final val ALL_KEY = "*"
1111
}
1212

13-
/** State objects * */
13+
/**
14+
* The in-memory state of the entity actor
15+
*/
1416
trait State {
1517
def uuid: String
1618
}
@@ -20,8 +22,14 @@ package object model {
2022
def createdDate: Date
2123
}
2224

25+
/**
26+
* Marker trait for serialization with Protobuf
27+
*/
2328
trait ProtobufDomainObject
2429

30+
/**
31+
* Marker trait for serialization with Jackson CBOR
32+
*/
2533
trait CborDomainObject
2634

2735
trait ProtobufStateObject extends ProtobufDomainObject with State

core/src/main/scala/app/softnetwork/persistence/typed/EntityBehavior.scala

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,20 +285,26 @@ trait EntityBehavior[C <: Command, S <: State, E <: Event, R <: CommandResult]
285285
case _ => Effect.unhandled
286286
}
287287

288-
/** @param state
288+
/** This method is invoked whenever an event has been persisted successfully or when the entity is
289+
* started up to recover its state from the stored events
290+
*
291+
* @param state
289292
* - current state
290293
* @param event
291294
* - event to hanlde
292295
* @return
293-
* new state
296+
* new state created by applying the event to the previous state
294297
*/
295298
def handleEvent(state: Option[S], event: E)(implicit context: ActorContext[_]): Option[S] =
296299
event match {
297300
case _ => state
298301
}
299302

300-
/** @param state
301-
* - current entity state
303+
/** This method is called just after the state of the corresponding entity has been successfully
304+
* recovered
305+
*
306+
* @param state
307+
* - current state
302308
* @param context
303309
* - actor context
304310
*/

0 commit comments

Comments
 (0)