Event Sourcing Paradigms: Entity, Relationship, and Process-Centric

View Source

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.

Three Paradigms Overview

The Three Paradigms

AspectEntity-CentricRelationship-Centric (DCB)Process-Centric
Stream identityEntity IDContext nameProcess instance ID
What stream representsEntity's complete historyAll facts in a bounded contextBusiness process execution log
Aggregate purposeEntity state reconstructionAd-hoc decision model (query-time)Process context ("dossier")
Concurrency unitSingle entity's streamQuery-based tag intersectionSingle process instance
Cross-cutting concernsSagas, process managersTags + query-based lockingProcess 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)

%% 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)

%% 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


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

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)

%% 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:

ParadigmSolutionCode ComplexityRuntime ComplexityAuditability
Entity-CentricTwo streams + sagaHigh (saga logic, compensation)Medium (saga orchestration)Medium
DCBTags + query-based lockingMedium (tag indexing)Higher (double queries)Low (query-time)
Process-CentricProcess stream + read model guardsLow (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:

%% 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:

%% 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

References

Process-Centric / Dossier Model

  • ReckonDB documentation and this guide

Dynamic Consistency Boundaries (DCB)

Entity-Centric (Traditional)

Academic

  • Abadi: "Consistency Tradeoffs in Modern Distributed Database System Design"
  • Helland & Campbell: "Building on Quicksand"