Event-sourcing framework designed for scalability & performance, while relying on eventual consistency. The core is technology-agnostic and every integration point is an interface that can be implemented for any database and message broker fitting the framework's philosophy and contracts. Adapters for AWS DynamoDB, SNS/SQS are shipped as a reference implementation.
- Philosophy in principles
- Design and Architecture
- User guide
- Why the reference implementation is on AWS
- When to choose Events-caravan over Axon
- Q&A
- Yet to be built
- Contributing
- Further reading
- License
- Aim for horizontal scalability. Anything that must be "the one" instance eventually becomes the ceiling. Partition the data, avoid hot partitions, shard.
- Rely only on simple and cheap technology features. Prefer smart endpoints and dumb pipes: no central orchestrator for business or technical processes, no broker that has to understand your domain. This makes it easy for the utilized technology to scale.
- Have one write destination for each operation result, between databases and message brokers, so there is no problem coordinating writes between multiple destinations and necessity to span transactions across them.
- Be satisfied with Eventual Consistency beyond the single entity, in the sense of Werner Vogels' "Eventually Consistent". The entity (aggregate root) is the only unit of strong consistency, as in Pat Helland's "Life beyond Distributed Transactions".
- Stay available. Build processes modifying one entity at a time, asynchronous to each other. Do not hold locks.
- Make compromises visible for devs. Where a guarantee is traded away (ordering, exactly-once delivery, strong consistency, small consistency units), developers are informed explicitly and are given the tools to compensate.
- Produced data must be durable. An event written to the store survives anything short of losing the store itself; Events must be delivered and processed by consumers at least once.
- Be ready for a failure. Software and hardware may break mid-process. Make every process recoverable. After a failure a retry should happen and finish the job without leaving the system in inconsistent state.
- Simple is secure. Design simple solutions. Utilize few dependencies.
- Modules should be substitutable. Every integration point (
EventStore,EventProducer,SnapshotStore, (de)serializers, the polling transport, the events publisher) is an interface, implementations of which could vary. - Developer experience is important. Events-caravan provides simple interfaces, no DSL, which help devs to stay focused on building domain behavior; framework has no complex mechanisms that require configuring, and it fails fast when misconfigured.
{
"entityReference": {
"entityName": "calculator",
"entityId": "1123456"
},
"eventName": "number-added",
"sequenceNumber": 15,
"timestamp": "2025-10-20T10:20:30.000Z",
"payload": {
"number": 101
}
}EventType and its payload are defined by combination of entityName and eventName. Payload class has to
be registered.
EventSourcedEntityderives its current state from all the events that happened to it. See usage- Each Entity is identified by a
EntityReference, which is a combination ofentityNameandentityId. - Each Event recorded on an Entity is identified by a combination of
EntityReferenceandsequenceNumber. - Each Event has its
eventName, which identifies how it alters state of an entity when applied on it. - Events are gaplessly sequenced per each entity starting from
sequenceNumber1. EntityReferencedefines a partition where its events are stored.- As an entity is the only unit of strong consistency per projects philosophy, the underlying event store reserves its freedom to store each partition in different machines for scaling horizontally freely.
- For entities with long histories Snapshotting can be utilized to avoid loading and re-applying all historic events from the beginning.
- Entities with long histories can make a partition where its events are stored too large, turning it into a scaling
bottleneck. To avoid that an
EntityReferencepartition is sharded consistently while events' sequenceNumbers increase. In the reference implementation module of DynamoDB partition-key isentityName#entityId#shardIndexwhere oneshardIndexincrements every N events defined bypartition-shard-sizeproperty. Shard indexes are incremental, and sort-key issequenceNumber, so all events are sorted in each sharded partition. BecauseshardIndexis derived fromsequenceNumberandpartition-shard-size,partition-shard-sizeparameter must stay fixed for the lifetime of the table.
- Domain methods on an
EventSourcedEntityrecord newEvents; and when entities are saved inEventSourcedRepositoryall newly recorded events are appended toEventStore. See usage - It's possible to produce multiple events atomically (up to 100 with AWS DynamoDB) when saving an
EventSourcedEntity, but it's recommended to limit to one event per operation. That saves the database from potential duty having to span a transaction across multiple partitions and writer nodes, which is a more complex and costly operation. - Events that do not belong to an
EventSourcedEntitycan be produced usingEventProducer.produce(...)interface directly. - Every produced event is an immutable document inserted into database. Inserting an event is publishing it. The database's inserts-stream (Change Data Capture mechanism) is read by an asynchronous Events-publisher and events are eventually propagated to consumers. This way there's no dual-write problem, no 2PC, and no need to maintain a transactional outbox, which could become a bottleneck or introduce technical complexities when scaling horizontally. On the other hand the database's inserts-stream is sharded together with the database itself, each shard naturally maintaining a separate stream.
- In the reference implementation
DynamoDB Streamsare read by aAWS Lambdarunning onNode.jsruntime publishing the event messages into anSNStopic per entityName.
- Event consumption mechanism can be implemented in various ways, but the reference implementation is based on queue-polling mechanism provided in corresponding module.
- Provided reference implementation uses
SQSqueues owned by consumer application and per subscribed topic belonging to an entityName. entityNameandeventNameare provided as String typeSNS MessageAttributes, which can be utilized for filtering when subscribing SQS queues to SNS topics.- Application layer utilizes
HandlerBasedEventConsumer, which provides possibility to consume events registeringEventHandlerimplementations. See usage. EventHandlersare needed to react on interested events or execute a new command.- The Event consumption mechanism and EventHandlers are not restricted to events produced by the same application, or
only to events recorded and produced within
EventSourcedEntity. - Only a successful consumption acknowledges an event message. If
EventHandlerthrows an exception, the message is not acknowledged, and may be redelivered. - A handler that throws an Exception leaves its message in the queue for redelivery; configure the queue's redrive policy and dead-letter queue so a permanently failing message does not retry forever.
- All matching
EventHandlersare tried and called upon each event message delivery. - Handlers or processes triggered by them must be idempotent and tolerant of out-of-order arrival (see Compromise #2).
- Event consumption mechanism is entirely optional, if application does not need to consume events.
- The queue-polling mechanism scales vertically adapting to the load, performs partial polls in cases if message consumption is not uniform across messages, batches deletes, supports graceful shutdown.
- Only uniqueness constraint and primary key index maintained by the underlying database is
(entityReference, sequenceNumber). - Upon event production a
DuplicateEventProductionExceptionmight be thrown for two reasons both having to do with entity modification in parallel or in short span of time:- another
Eventof the samesequenceNumberwas produced in parallel after theEventSourcedEntitywas loaded. - an entity was loaded in a stale state without the newest events due to inconsistency between database's reader nodes.
- another
- In case of
DuplicateEventProductionExceptionthe failed operation should be retried from the beginning until it's successfully processed or recognized as outdated, and acknowledged in any case.
The event-store structure of events-caravan provides answer to exactly one query: find events of one entity, ordered.
EventSourcedRepository provides possibility of re-creating an entity's single projection using @ApplyEvent
annotation. This projection is utilized for responding to commands and writing new events further mutating the entity's
state. Thus, this projection is called write-model.
However, for more complex queries other projections of the entity called query-models must be utilized. The query
models may and should have different structures, lifecycles, separate indexes, or even separate RDBMS than that of the
write-model. (See CQRS). For example a PostgreSQL database table that
contains only specific fields of entities, which are in specific status at the given moment; or utilizing a search
engine such as Elasticsearch. Therefore, the events-caravan leaves complete freedom for defining how those query models
are to be constructed and maintained. The framework provides and recommends EventHandlers for maintaining the
query-models asynchronously from command-handling writer processes.
Note
Per Principle #6, every guarantee traded away for scalability is listed here explicitly, together with how to compensate for it.
-
The most important compromise of Events-caravan compared to another popular event-sourcing framework, Axon, is that Events-caravan does not maintain a global sequence of events across all entities. Therefore, there's no global iterable stream of events. Such a compromise is taken because otherwise a global ordering mechanism would be necessary; This central orchestrator would become a bottleneck of the system upon writing, preventing writer processes from scaling. Events-caravan avoids this by partitioning at the entity level, enabling more elastic horizontal scaling.
This approach has the following drawbacks:
- CQRS query-model reflections cannot be re-built by retriggering
EventHandlersfor all historic events. - There's no built-in event-store in case if events across many entities need to be republished.
If the mentioned features are important, there are compensations for the compromise:
- A stream of entity references; and mechanism of re-building CQRS query-models per entity.
- A separate globally-sorted event-log;
can be maintained. Both should be populated asynchronously and stay eventually consistent with the events-caravan's event-store as per the project's philosophy. Events-caravan ships the first one out of the box: see VII. Set up an optional Entity-Stream.
- CQRS query-model reflections cannot be re-built by retriggering
-
Events delivery to consumers is at-least (not exactly) once and unordered. Consumers own idempotency and, if needed, ordering. Every event message carries a gapless
sequenceNumberstarting at 1 perentityReference, which might be helpful for deduplication and reordering by an application. Ultimate deduplication and reordering is recommended to be taken care of by an underlying domain layer. e.g. Do not brew coffee#7, if the coffee#7 is already in status delivered. -
Since due to the project's philosophy the only unit of consistency is a single Entity, if there's a necessity to maintain consistency across multiple Entities, applications must respond to this. The answer to this challenge is Sagas.
-
Optimistic locking triggers late, and may waste hardware resources if clashes happen too often. DynamoDB's
consistent-readcould be used to reduce loading stale entity states, which can be enabled via properties. But it's twice cheaper to avoid it, unless frequent parallel writes to same entities are anticipated. On top of that custom pessimistic locking and in-memory retry mechanisms can be applied on the entities that expect frequent clashes. -
With the reference technology adapters, events must fit DynamoDB item (400 KB) and SNS message (256 KB) limits.
For processes spanning multiple entities or services,
prefer choreography-based sagas: each step is an EventHandler
that records the next event, and if process fails due to domain logic, compensation is just another event reverting
previous operations. This needs no machinery beyond what the events-caravan provides and keeps the pipeline free of
orchestrators, in line with the philosophy. If a domain-process genuinely needs a central state, model the process
itself as an EventSourcedEntity. Its events history then documents the workflow.
| Module | Purpose |
|---|---|
| events-caravan-core | Core, technology-agnostic: entities, repositories, event registry, interfaces, Jackson serialization, event consumption and handlers, entity stream |
| events-caravan-dynamodb | EventStore + EventProducer + SnapshotStore + EntityStreamWriter implementations on DynamoDB, with partition sharding for long entity histories |
| events-caravan-queue-polling | Transport-agnostic continuous polling mechanism (virtual threads, adaptive poller scaling, batched deletes, graceful shutdown) |
| events-caravan-sqs | SQS polling/deletion primitives |
| events-caravan-spring-boot-starter | Auto-configuration of the core components |
| events-caravan-dynamodb-spring-boot-starter | Auto-configuration of the DynamoDB event, snapshot and entity stream stores |
| events-caravan-sqs-spring-boot-starter | Auto-configuration of per-entity SQS queue polling |
| dynamodb-sqs-events-publisher-lambda | Node.js based Lambda publishing events from DynamoDB's stream into SNS (its README) |
| events-caravan-integration-tests | End-to-end tests against a local AWS simulator |
Add the starters your service needs:
<dependencies>
<dependency>
<groupId>dev.baitursinov</groupId>
<artifactId>events-caravan-dynamodb-spring-boot-starter</artifactId>
<version>${events-caravan-version}</version>
</dependency>
<!-- only if this service consumes events -->
<dependency>
<groupId>dev.baitursinov</groupId>
<artifactId>events-caravan-sqs-spring-boot-starter</artifactId>
<version>${events-caravan-version}</version>
</dependency>
</dependencies>Note
-
If default Jackson based (de)serializers are to be used, the events-caravan's spring-boot-starter does not bring in the
JsonMapperbean it requires. Your application should configure and provide it. The default Jackson-based serialization activates when Jackson 3 is on the classpath and aJsonMapperbean is provided in Spring context. -
Alternatively, provide your own
EventSerializer,EventPayloadSerializer,EventDeserializer,EventPayloadDeserializer,SnapshotSerializerandSnapshotDeserializerbeans to use a different serialization mechanism. -
DynamoDbClient/SqsClientbeans are expected in the context for their corresponding modules to work. -
Every autoconfigured component is
@ConditionalOnMissingBean, supply your own bean to override them. AnyEventProducerbean is transparently wrapped intoValidatingEventProducerso all produced events are validated against theEntityEventsRegistry.
@Configuration
public class CalculatorEventsConfiguration {
@Bean
public EntityEventsRegistration calculatorEventsRegistration() {
return new EntityEventsRegistration(
"calculator",
Map.of(
"number-added", NumberCarryingPayload.class,
"number-subtracted", NumberCarryingPayload.class));
}
}Note
-
A registered entity does not have to be produced locally: you can register events produced by another application to react to them in your application.
-
Entity and Event names are identified by explicit Strings, not Java class names, so payload classes can be renamed and moved freely without breaking stored history.
-
The registry is built once at startup. Application validates every event produced or applied against the registry.
-
EntityEventsRegistration is picked up by Spring-boot automatically.
The matching @ApplyEvent method mutates in-memory state immediately when recording a new event and when the entity is
later loaded by replaying its historical events.
Entity state mutates further when new events are recorded by EventSourcedEntity.recordEvent().
@EntityName("calculator")
public class Calculator extends EventSourcedEntity {
private final String id;
long currentNumber = 0;
public Calculator(String id) {
this.id = id;
}
public long currentNumber() {
return this.currentNumber;
}
public void addNumber(long number) {
recordEvent("number-added", new NumberCarryingPayload(number));
}
@ApplyEvent("number-added")
private void applyAddNumber(Event<NumberCarryingPayload> event) {
this.currentNumber += event.payload().number();
}
@Override
public String entityId() {
return id;
}
}Tip
- Apply methods can also live outside the entity class (see
@ApplyEventand@EventApplier), keeping domain classes free of state change mechanics.
It's recommended to place external @EventApplier classes in the same package as Entity in order to utilize package-private fields/methods in order to mutate Entity's state while applying its events. This helps Entity not to expose public getter/setter methods just for applying events without real domain behavior.
@Repository
public class CalculatorRepository extends EventSourcedRepository<Calculator> {
public CalculatorRepository(EventSourcingRepositoryContext context) {
super(Calculator.class, context);
}
@Override
protected Calculator createWithBlankState(String entityId) {
return new Calculator(entityId);
}
}Use it like any repository:
public class CalculatorService {
private final CalculatorRepository calculatorRepository;
public CalculatorService(CalculatorRepository calculatorRepository) {
this.calculatorRepository = calculatorRepository;
}
void addToMagicNumber() {
var calculator = new Calculator("42");
calculator.addNumber(10);
calculatorRepository.save(calculator);
Optional<Calculator> foundCalculator = calculatorRepository.findBy("42");
}
}Note
- Entities in a blank state (no events recorded) cannot be saved or loaded.
- Saving produces all events recorded since the entity was loaded.
For entities with long histories, register a SnapshotTaker bean. An EventSourcedRepository then persists a snapshot
every N events defined by frequencyOfSnapshots. When the entity is loaded, the framework restores its state from the
latest snapshot plus later events instead of having to load and re-apply all events.
Snapshot writes are deliberately not atomic with event production to avoid the dual-write problem. A failed snapshot is simply retaken at the next opportunity, and failure to take snapshot does not corrupt an entity's state.
@Component
public class CalculatorSnapshotTaker extends SnapshotTaker<Calculator, CalculatorSnapshot> {
public CalculatorSnapshotTaker() {
super(Calculator.class, CalculatorSnapshot.class);
}
@Override
public CalculatorSnapshot takeSnapshot(Calculator entity) {
return new CalculatorSnapshot(entity.currentNumber());
}
@Override
public Calculator recreateFromSnapshot(EntityReference entityReference,
CalculatorSnapshot snapshotPayload) {
var result = new Calculator(entityReference.entityId());
result.currentNumber = snapshotPayload.currentNumber();
return result;
}
@Override
public int frequencyOfSnapshots() {
return 50;
}
}Note
-
Spring Boot starter wires it into the
EventSourcingRepositoryContextautomatically. -
SnapshotTaker in the provided example is located in the same package as Calculator entity, and uses its package-private fields to create its snapshot and recreate the Calculator from snapshot. This helps Calculator entity not to expose public getter/setter methods just for snapshotting without real domain behavior.
Handlers are matched by payload type, then filtered by isOfInterest (typically on the EventType):
@Component
public class NumberAddedHandler implements EventHandler<NumberCarryingPayload> {
private static final EventType INTERESTED_EVENT_TYPE = new EventType("calculator", "number-added");
@Override
public boolean isOfInterest(Event<NumberCarryingPayload> event) {
return INTERESTED_EVENT_TYPE.equals(event.eventType());
}
@Override
public void handle(Event<NumberCarryingPayload> event) {
// update a query-model, execute a command, ...
}
}Note
- Handlers are registered automatically by Spring boot starters.
Per Compromise #1, Events-caravan keeps no global, cross-entity stream of events. Events are sorted in scope of each entity only. So in case if all entities or events must be iterated over (e.g. CQRS query-models need to be rebuilt), an optional functionality Entity-stream compensates for absence of global sequence of events.
With the DynamoDB starter, setting caravan.event.sourcing.entity-stream.dynamo-db.table-name property autoconfigures a
DynamoDbBasedEntityStreamWriter, and its caller EntityStreamWritingEventHandler
(see Configure via application properties).
Which entities are written into the stream, and at which granularity, is declared per entityName via an
EntityStreamRegistration bean:
@Configuration
public class CalculatorEventsConfiguration {
@Bean
public EntityStreamRegistration calculatorEntityStreamRegistration() {
return new EntityStreamRegistration("calculator", TimeBucket.MONTHLY, 4);
}
}EntityStreamWritingEventHandler partitions entity references by their:
entityName- first event timestamp's
timeBucketgranularity - within each bucket into N shards (
shardCount) by a hash (FNV-1a 64-bit) ofentityId
then passes the derived time bucket and shard locations along with the entity reference to the configured
EntityStreamWriter. This way the Entity-stream's storage is enabled to scale horizontally without hot partitions.
Important
-
EntityStreamRegistration'stimeBucketandshardCountmust stay fixed per entityName` once the Entity-stream table has entities of it in it. Re-creating the stream for already written data changing their sharding parameters may be an expensive operation. -
If
EntityStreamRegistrationis not set up, entities won't be populated for theentityName. Setting it up later will start populating only the new entities into the stream.
Note
-
This functionality is entirely optional; without an
EntityStreamWriterbean, the Entity-stream is not populated. However, the functionality must be enabled in advance, as there's no way of populating the Entity-stream for Entities having been created in the past. -
Entities whose
entityNamehas noEntityStreamRegistrationare not written into the Entity-stream, so different entities can be onboarded onto the Entity-stream independently, at whatever granularity suits their event production frequency. -
timeBucketandshardCountmust stay fixed perentityNameonce the table has entities of it in it, as they define how the stream is sharded. Thus, it's important to set values to have fine enough partitions in anticipation of the load. -
On the other hand, setting the values too generously (small time-buckets and high shard count) will lead to more partitions, which would be empty or underfilled, when Entity creation is not frequent enough. Consequently, iterating over the Entity-stream will be more costly due to some DB queries will result in no or few entries.
-
Wiring of components is automatic once beans exist:
EntityStreamRegistrationbeans are collected into anEntityStreamRegistry, andEntityStreamWritingEventHandleris registered by the Spring boot starter and calls it for every registered entity's first event (sequenceNumber == 1).
caravan:
event:
sourcing:
event-store:
dynamo-db:
table-name: my-app_events
partition-shard-size: 10000 # events per partition-key shard
query-max-page-size: 1000 # events loaded into memory per page while replaying an entity
consistent-read: false # opt into strongly consistent reads if needed
snapshot-store:
dynamo-db:
table-name: my-app_snapshots
consistent-read: false # opt into strongly consistent reads if needed
entity-stream:
dynamo-db:
table-name: my-app_entity-stream # absent by default: entity stream is off unless this is set
messaging: # only with the SQS starter
queue-name-prefix: my-app # queues are named {prefix}_{entityName}
subscribed-entities:
- calculator
graceful-shutdown-seconds: 10 # set 0 for immediate shutdown
concurrency: 10 # max in-flight messages per queue
max-poll-size: 10 # max messages requested per poll
min-poll-size: 3 # min free capacity worth polling for
pollers-count-cap: 0 # max poller threads per queue; 0 = derived from concurrency and max-poll-size
poll-wait-seconds: 10 # long-poll wait per request
deletion: # batching of consumed-message deletions
max-batch-size: 10
period-seconds: 1
concurrency: 3Note
All values except the table names, queue-name-prefix and subscribed-entities properties are shown at their
defaults and can be omitted. entity-stream.dynamo-db.table-name has no default: it is what turns the optional
Entity-stream on.
Important
event-store.dynamo-db.partition-shard-size is baked into how items are keyed and must be fixed once the events
table is populated.
Events-caravan provides no infrastructure, and is infrastructure-agnostic. A deployment utilizing the reference
implementation based on AWS must provide: the events and snapshots DynamoDB tables, plus the entity-stream DynamoDB
table if Entity-Stream is enabled. The events table should have
NEW_IMAGE stream enabled, for which a Node.js based Lambda code is provided, which can be used as a reference for
publishing events. It's used for integration-tests as well. The Lambda code itself is documented in
the Lambda's README.
Tip
For local development, ./local/env-up provisions the entire pipeline (tables, stream, Lambda, topics, queues)
against a local AWS simulator run by Docker Compose; and ./local/test runs the full integration test suite against
it.
Why the reference implementation is on AWS, why it suits the project's philosophy and what could substitute it
Services that are utilized by events-caravan are serverless, and horizontal scaling is provided by AWS itself. This is the primary reason why these technologies were chosen. Events-caravan utilizes them without constraining the underlying infrastructure from scaling.
- DynamoDB partitions natively by primary key, has no single leader node applications write through, and scales throughput per partition rather than globally, directly serving Principle #1 (horizontal scalability) and Principle #5 (stay available, no locks). DynamoDB Streams give an ordered, per-shard change feed for free, which is what lets an event item insert be equal to the publishing it (see Producing and propagating events point 4) without a transactional outbox, serving Principle #3 (one write destination).
- SNS/SQS is a broker that does not need to understand the domain: SNS fans out by opaque message attributes, and SQS queues are owned and scaled independently by each consumer application. This is "dumb pipes" per Principle #2, there is no central bus or orchestrator coordinating consumers.
- The publisher Lambda only moves records from DynamoDB Streams to SNS; it holds no business logic and scales with the number of stream shards, so it does not become a bottleneck or a single point of failure, consistent with Principle #1.
None of this is AWS-specific in substance, only in the concrete API used, and
per Principle #10 every integration point is an interface open for substitution
(EventStore, EventProducer, SnapshotStore, event consumption, the publisher).
Note
When operating applications on a high scale it might be worth switching to provisioned capacity of DynamoDB, with reserved instances. However, if very high-scale load is expected, it might be worthy to plan utilizing different technologies not to pay AWS per request, but operate your own DB and Message broker. On the other hand that might increase operational cost of the system.
- A partitioned database with a native, ordered per-partition change feed can substitute DynamoDB: for example Cassandra/ScyllaDB with CDC.
- A broker letting consumers own and scale their own subscriptions without a central process understanding the domain can substitute SNS/SQS: for example Kafka, Pulsar, or Google Pub/Sub.
- The publisher can be substituted by a stream-reader process as long as it preserves at-least-once delivery and keeps the insert as the sole trigger for publishing.
Events-caravan and Axon solve the same problem, which is event sourcing and CQRS, but with opposite approach on centralization, driven by Compromise #1:
- Choose Events-caravan when horizontal scalability without a central component is the priority: there is no global events sequence and duty to maintain it. Library's footprint is a handful of interfaces over your own database and broker, which suits teams already operating DynamoDB/SNS/SQS-shaped infrastructure, or willing to implement the equivalent adapters; and comfortable maintaining their own sorted query-model or global event-log, if they actually need one (see Compromise #1).
- Choose Axon when a global, replayable stream of all events is needed out of the box. For example rebuilding many projections from scratch across every entity, without building that yourself, or when Axon Server's built-in tracking processors, deadline manager, and sagas orchestration outweigh the cost of running and scaling a central server.
In short: Events-caravan trades Axon's built-in global event-store and orchestration machinery for horizontal scalability with fewer moving parts; pick whichever side of that trade fits your team's operational scale and appetite for infrastructure.
-
Q: Is AI (LLM) utilized when developing Events-caravan?
- A: Yes, for coding, documenting and brainstorming it was useful. However, its output, especially concerning important software components, were carefully read and integrated after analysis. I found it important to stay on top of code changes; to be AI's driver, but also learn from it and apply its suggestions only with human judgment.
-
Q: How to design Aggregates (DDD)?
- A: EventSourcedEntity as single unit of consistency, which can serve as an Aggregate root. Its state may contain other domain entities or value objects; and it may maintain business invariants within itself.
-
Q: Why isn't an Inbox for events-messages to technically deduplicate them based on EventReference to achieve idempotence?
- A: having an "Inbox" table means that application needs to populate it after processing each message. If it's done in a separate transaction, appearance of the Inbox entry after a domain-write is not guaranteed. If saving the entry is done in the transaction scope as the domain entity writes, it'll contradict the philosophy of the framework: the transaction would have to span two tables (or partitions, if went with single DynamoDB table), potentially across several machines. Which has its costs when it comes to scalability, and in fact it's just a more costly operation in AWS.
- Introduce a mechanism of iterating over Entity-Stream and re-building CQRS query-models.
- Introduce events flow traceability, metrics.
- Introduce Event versioning and upcasting mechanism.
- Develop adapters for ScyllaDB + Debezium + Kafka protocol.
- Introduce Multi region scalability.
- Provide possibility of taking snapshots async from thread of
EventSourcedRepository.save(...); - Provide optional capability not to send Event payload into message broker, but only reference to be used for fetching the event details from the event-store.
- Support to have
@ApplyEventparameter as unwrapped payload (withoutEvent<T>).
Events-caravan is created and currently maintained by Sagynysh Baitursinov. Bug reports, feature proposals, and pull requests are welcome — see the contributing guide for the development setup and workflow, and the code of conduct for community standards. Security vulnerabilities should be reported privately as described in the security policy.