# Event Sourcing Paradigms: Entity, Relationship, and Process-Centric
This guide explores three distinct approaches to event sourcing, each with different mental models, trade-offs, and use cases. Understanding these paradigms helps you choose the right approach for your domain.

## The Three Paradigms
| Aspect | Entity-Centric | Relationship-Centric (DCB) | Process-Centric |
|--------|----------------|---------------------------|-----------------|
| **Stream identity** | Entity ID | Context name | Process instance ID |
| **What stream represents** | Entity's complete history | All facts in a bounded context | Business process execution log |
| **Aggregate purpose** | Entity state reconstruction | Ad-hoc decision model (query-time) | Process context ("dossier") |
| **Concurrency unit** | Single entity's stream | Query-based tag intersection | Single process instance |
| **Cross-cutting concerns** | Sagas, process managers | Tags + query-based locking | Process flow owns all participants |
| **Mental model** | "Entity has history" | "Facts are tagged for slicing" | "Dossier passed desk to desk" |
---
## Entity-Centric Paradigm (Traditional DDD)
The most common approach, derived from Domain-Driven Design. Each entity (aggregate) owns a dedicated event stream.
### Mental Model
> "Every entity has a history. The entity IS its history."
```
Stream: order-123
├── OrderPlaced
├── ItemAdded
├── PaymentReceived
├── OrderShipped
└── OrderDelivered
Stream: user-456
├── UserRegistered
├── ProfileUpdated
├── PasswordChanged
└── AccountVerified
```
### Characteristics
**Strengths:**
- Intuitive mapping to domain objects
- Clear ownership: one stream per aggregate
- Well-understood patterns (DDD literature)
- Simple optimistic concurrency (stream version)
**Weaknesses:**
- Cross-entity invariants require sagas
- Fact duplication when entities interact
- Saga complexity grows with domain complexity
- Compensating transactions for failure handling
### Example: Course Enrollment (Entity-Centric)
```erlang
%% Two streams, fact duplicated
%% Stream: course-CS101
%% ├── CourseCreated
%% └── StudentEnrolled {student_id: 456} ← FACT 1
%%
%% Stream: student-456
%% ├── StudentCreated
%% └── CourseJoined {course_id: CS101} ← SAME FACT, DUPLICATED
%% Saga required to coordinate:
%% 1. Check student course count < 10
%% 2. Check course student count < 30
%% 3. Append to course stream
%% 4. Append to student stream
%% 5. If step 4 fails, compensate step 3
```
### When to Use
- Domains where entities are truly independent
- Simple CRUD-like operations
- Well-bounded aggregates with few cross-entity invariants
---
## Relationship-Centric Paradigm (Dynamic Consistency Boundaries)
Introduced by Sara Pellegrini in her "Killing the Aggregate" thesis. Events are tagged with multiple entities, enabling multi-dimensional querying.
### Mental Model
> "Events are facts about relationships, not entities. Facts can be sliced by any participating entity."
```
Stream: enrollment-context (single stream for all enrollments)
├── Enrolled {student: 456, course: CS101, tags: [student:456, course:CS101]}
├── Enrolled {student: 456, course: MATH201, tags: [student:456, course:MATH201]}
├── Enrolled {student: 789, course: CS101, tags: [student:789, course:CS101]}
└── ...
Query by tags:
- "course:CS101" → 2 enrollments
- "student:456" → 2 enrollments
```
### Characteristics
**Strengths:**
- No fact duplication
- No sagas for cross-entity invariants
- Query-time flexibility
- Single source of truth for relationships
**Weaknesses:**
- Requires tag indexing infrastructure
- Double-query cost (decision + precondition check)
- Query-time aggregation loses temporal context
- Mental model shift from traditional DDD
### Example: Course Enrollment (DCB)
```erlang
%% Single stream with tags
Event = #{
event_type => enrollment_requested,
data => #{student_id => 456, course_id => <<"CS101">>},
tags => [<<"student:456">>, <<"course:CS101">>]
},
%% Query-based concurrency check:
%% 1. Query events matching "course:CS101" → get count + last position
%% 2. Query events matching "student:456" → get count + last position
%% 3. Append with precondition: no new events matching either query since position
Query = {and, [{tags, [<<"course:CS101">>]}, {tags, [<<"student:456">>]}]},
reckon_db:append(<<"enrollments">>, [Event], {query, Query, LastPosition}).
```
### When to Use
- Domains dominated by relationships between entities
- Cross-entity invariants are common
- Analytical/reporting needs require multi-dimensional slicing
- Willing to invest in tag indexing infrastructure
### References
- [DCB.events](https://dcb.events/) - Official documentation
- [Sara Pellegrini's Event Thinking](https://sara.event-thinking.io/)
- [Axon Framework 5 DCB Support](https://www.axoniq.io/blog/dcb-in-af-5)
- [EventSourcingDB DCB Best Practices](https://docs.eventsourcingdb.io/best-practices/dynamic-consistency-boundaries/)
---
## Process-Centric Paradigm (The Dossier Model)
**ReckonDB's recommended approach.** An event stream represents a business process instance, not an entity. The aggregate is the accumulated context - like a dossier passed from desk to desk.
### Mental Model
> "An event stream is an ordered log of facts that represent a business process. The aggregate is the 'dossier' - accumulated context passed from desk to desk, gradually filling with facts."

### The Dossier Metaphor
Imagine a loan application process in a traditional office:
```
┌─────────────────────────────────────────────────────────────────┐
│ THE DOSSIER - Physical folder passed between desks │
├─────────────────────────────────────────────────────────────────┤
│ │
│ DESK 1 (Intake): │
│ └─ Application form received │
│ └─ Dossier starts: applicant name, requested amount │
│ │
│ DESK 2 (Credit Check): │
│ └─ Credit report pulled │
│ └─ Dossier gains: credit score, debt-to-income ratio │
│ │
│ DESK 3 (Underwriting): │
│ └─ Risk assessment completed │
│ └─ Dossier gains: approval decision, conditions, notes │
│ │
│ DESK 4 (Documentation): │
│ └─ Documents verified │
│ └─ Dossier gains: verification checklist, issues found │
│ │
│ DESK 5 (Closing): │
│ └─ Loan disbursed │
│ └─ Dossier complete: final terms, disbursement details │
│ │
└─────────────────────────────────────────────────────────────────┘
The PROCESS owns the stream.
The applicant, the loan, the credit bureau - all are PARTICIPANTS.
```
### Characteristics
**Strengths:**
- Complete auditability: dossier records WHAT WE KNEW at each decision
- Single stream per process instance (no coordination)
- Invariants checked as process guards using read models
- Natural fit for workflow-heavy domains
- "Why did we approve this?" → replay the dossier
**Weaknesses:**
- Requires read models for cross-process queries
- Less intuitive for pure entity-focused domains
- May need to denormalize entity state across processes
### Example: Course Enrollment (Process-Centric)
```erlang
%% Process: EnrollmentRequest
%% Stream: enrollment-request-{uuid}
-record(enrollment_dossier, {
request_id,
student_id,
course_id,
%% Context captured at decision time
student_courses_at_request = [],
course_students_at_request = [],
%% Process outcome
validation_status,
outcome,
outcome_reason
}).
%% Command handler: check invariants as process GUARDS
handle_command(#request_enrollment{student_id = S, course_id = C}, _State) ->
%% Fetch current state from READ MODELS (projections)
StudentCourses = enrollments_projection:courses_for_student(S),
CourseStudents = enrollments_projection:students_for_course(C),
%% Guard: check both invariants BEFORE emitting events
case {length(StudentCourses) < 10, length(CourseStudents) < 30} of
{true, true} ->
%% Dossier records what we knew at decision time
[#enrollment_requested{
student_id = S,
course_id = C,
student_courses_at_request = StudentCourses,
course_students_at_request = CourseStudents
}];
{false, _} ->
[#enrollment_rejected{
student_id = S,
course_id = C,
reason = student_course_limit_reached,
student_course_count = length(StudentCourses)
}];
{_, false} ->
[#enrollment_rejected{
student_id = S,
course_id = C,
reason = course_capacity_reached,
course_student_count = length(CourseStudents)
}]
end.
%% Apply events to build dossier state
apply_event(#enrollment_requested{} = E, Dossier) ->
Dossier#enrollment_dossier{
student_id = E#enrollment_requested.student_id,
course_id = E#enrollment_requested.course_id,
student_courses_at_request = E#enrollment_requested.student_courses_at_request,
course_students_at_request = E#enrollment_requested.course_students_at_request,
validation_status = valid
};
apply_event(#enrollment_completed{}, Dossier) ->
Dossier#enrollment_dossier{outcome = completed};
apply_event(#enrollment_rejected{reason = R}, Dossier) ->
Dossier#enrollment_dossier{
outcome = rejected,
outcome_reason = R
}.
```
### The Key Insight: Temporal Context
The process-centric model captures **temporal context** that query-time aggregation cannot:
```
AUDITOR: "Why was this loan approved despite the applicant's low credit score?"
ENTITY-CENTRIC: "The aggregate says approved. Check the saga logs... somewhere."
DCB: "Query the events... but we only know the final state, not what
the underwriter saw at decision time."
PROCESS-CENTRIC: "Replay the dossier. At decision time (event 3),
the underwriter saw: credit score 620, but debt-to-income
was 28% (good), and applicant had 5 years at current job.
The dossier records all context that was available."
```
### When to Use
- Workflow-heavy domains (loan processing, insurance claims, order fulfillment)
- Regulatory/compliance requirements for decision auditability
- Processes with multiple participants that must be coordinated
- Long-running processes with multiple decision points
---
## Paradigm Comparison: The Enrollment Problem
All three paradigms can solve the same problem. Here's how they compare:
| Paradigm | Solution | Code Complexity | Runtime Complexity | Auditability |
|----------|----------|-----------------|-------------------|--------------|
| **Entity-Centric** | Two streams + saga | High (saga logic, compensation) | Medium (saga orchestration) | Medium |
| **DCB** | Tags + query-based locking | Medium (tag indexing) | Higher (double queries) | Low (query-time) |
| **Process-Centric** | Process stream + read model guards | Low (simple guards) | Low (single stream writes) | High (temporal) |
### Complexity Analysis
```
Entity-Centric:
┌─────────────────────────────────────────────────────────────┐
│ Components needed: │
│ - Course aggregate │
│ - Student aggregate │
│ - EnrollmentSaga (coordinates both) │
│ - CompensationHandler (rollback on failure) │
│ - 2 streams, 4 event types, saga state machine │
└─────────────────────────────────────────────────────────────┘
DCB:
┌─────────────────────────────────────────────────────────────┐
│ Components needed: │
│ - Tag index (new infrastructure) │
│ - Query-based concurrency (new append mode) │
│ - Decision model builder │
│ - 1 stream, 1 event type, 2 queries per write │
└─────────────────────────────────────────────────────────────┘
Process-Centric:
┌─────────────────────────────────────────────────────────────┐
│ Components needed: │
│ - EnrollmentRequest process (aggregate) │
│ - EnrollmentsProjection (read model) │
│ - 1 stream per request, 2-3 event types, simple guards │
└─────────────────────────────────────────────────────────────┘
```
---
## Choosing the Right Paradigm
### Decision Framework
```
START
│
▼
┌─────────────────────────────────────────┐
│ Is your domain workflow-heavy? │
│ (loan processing, claims, fulfillment) │
└─────────────────────────────────────────┘
│ │
YES NO
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────────────────┐
│ PROCESS-CENTRIC │ │ Do you have many cross-entity │
│ (Dossier Model) │ │ invariants? │
└─────────────────┘ └─────────────────────────────────┘
│ │
YES NO
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────┐
│ Can you invest in │ │ ENTITY-CENTRIC │
│ tag infrastructure? │ │ (Traditional) │
└─────────────────────┘ └─────────────────┘
│ │
YES NO
│ │
▼ ▼
┌───────────┐ ┌─────────────────┐
│ DCB │ │ PROCESS-CENTRIC │
└───────────┘ │ (simpler) │
└─────────────────┘
```
### Hybrid Approaches
Real systems often use multiple paradigms:
```erlang
%% Master data: Entity-centric
%% Stream: product-SKU123 (catalog item, rarely changes)
%% Stream: customer-C456 (profile data)
%% Transactional processes: Process-centric
%% Stream: order-process-ORD789 (order fulfillment)
%% Stream: return-request-RET012 (return processing)
%% Analytics: Tag-based queries (without DCB concurrency)
%% Query by tags for reporting, not for consistency
```
---
## ReckonDB's Position
**ReckonDB recommends the process-centric paradigm** for most event-sourced systems because:
1. **Better auditability** - The dossier captures decision context
2. **Simpler implementation** - No sagas, no tag indexes for concurrency
3. **Natural fit** - Most business operations are processes, not entity mutations
However, ReckonDB supports **tags for query purposes**:
```erlang
%% Tags for analytics, NOT for concurrency control
Event = #{
event_type => enrollment_completed,
data => #{
process_id => <<"enrollment-req-123">>,
student_id => 456,
course_id => <<"CS101">>
},
tags => [<<"student:456">>, <<"course:CS101">>] %% For queries only
},
%% Concurrency remains stream-version-based
{ok, V} = reckon_db:append(<<"enrollment-req-123">>, [Event], ExpectedVersion).
%% But you can query across processes by tags
{ok, AllStudentEnrollments} = reckon_db:read_by_tags([<<"student:456">>], #{}).
```
---
## Further Reading
- [Event Sourcing Guide](event_sourcing.md) - Core event sourcing concepts
- [CQRS Guide](cqrs.md) - Command Query Responsibility Segregation
- [Subscriptions Guide](subscriptions.md) - Real-time event notifications
- [Snapshots Guide](snapshots.md) - Optimizing aggregate loading
## References
### Process-Centric / Dossier Model
- ReckonDB documentation and this guide
### Dynamic Consistency Boundaries (DCB)
- [DCB.events](https://dcb.events/) - Official DCB documentation
- [Sara Pellegrini's Event Thinking](https://sara.event-thinking.io/) - "Killing the Aggregate" thesis
- [Kill Aggregate? Interview](https://docs.eventsourcingdb.io/blog/2025/12/15/kill-aggregate-an-interview-on-dynamic-consistency-boundaries/) - Bastian Waidelich, EventSourcingDB
- [DCB in Axon Framework 5](https://www.axoniq.io/blog/dcb-in-af-5) - First major framework support
- [Axon Server 2025.1](https://www.axoniq.io/blog/axon-server-future-proof-event-store) - Event store with DCB
### Entity-Centric (Traditional)
- Martin Fowler: [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)
- Greg Young: [CQRS and Event Sourcing](https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf)
- Vaughn Vernon: "Implementing Domain-Driven Design"
### Academic
- Abadi: "Consistency Tradeoffs in Modern Distributed Database System Design"
- Helland & Campbell: "Building on Quicksand"