Event Contract Mental Model: Facts, Commands, Notifications, and State Transfer
Learn Java API Contract Engineering, Event Contract Engineering & Schema Governance - Part 013
Event contract mental model for Java engineers: facts, commands, notifications, state transfer, event identity, causality, correlation, ordering, and semantic compatibility.
Part 013 — Event Contract Mental Model: Facts, Commands, Notifications, and State Transfer
Tujuan Pembelajaran
Pada part sebelumnya kita menyelesaikan lapisan utama HTTP API contract engineering: request/response, error, versioning, OpenAPI Java integration, client SDK, dan contract testing. Sekarang kita masuk ke event contract engineering.
Masalah umum engineer menengah adalah menganggap event contract sama dengan payload schema:
event contract = JSON/Avro/Protobuf schema
Itu terlalu dangkal.
Dalam sistem produksi, event contract mencakup:
- makna domain dari sesuatu yang terjadi;
- siapa yang menyatakan fakta itu;
- kapan fakta itu terjadi;
- apa identity event tersebut;
- apa entity/agregat yang dipengaruhi;
- apa relasi kausal dengan command/event lain;
- apakah event bisa direplay;
- apakah consumer boleh mengambil keputusan dari event;
- apakah event bersifat immutable;
- bagaimana event berevolusi tanpa merusak consumer;
- bagaimana ordering, idempotency, dan deduplication ditangani;
- bagaimana event diamati, diaudit, dan di-govern.
Setelah part ini, kamu harus mampu:
- membedakan event, command, notification, message, dan state transfer;
- mendesain event sebagai domain fact, bukan remote procedure call tersembunyi;
- menentukan kapan payload harus fact-oriented, state-oriented, atau notification-oriented;
- memahami event identity, aggregate identity, causation ID, correlation ID, trace ID;
- membedakan event time, publish time, processing time, dan ingestion time;
- menganalisis compatibility event dari sisi semantic, bukan hanya schema;
- menghindari anti-pattern event-driven architecture yang membuat sistem rapuh.
1. Kaufman Skill Slice
Skill utama part ini:
“Mampu mendesain event contract yang bisa dipakai banyak consumer tanpa membuat coupling tersembunyi, semantic ambiguity, atau distributed workflow chaos.”
Sub-skill:
| Sub-skill | Output praktis |
|---|---|
| Event classification | Bisa membedakan fact, command, notification, state transfer |
| Naming discipline | Event name merepresentasikan domain fact, bukan implementation action |
| Identity model | Event ID, aggregate ID, entity ID, correlation ID, causation ID jelas |
| Time model | occurredAt, publishedAt, processedAt tidak tercampur |
| Semantic boundary | Payload tidak membocorkan internal transaction/domain object sembarangan |
| Consumer reasoning | Bisa menjelaskan apa yang boleh diasumsikan consumer dari event |
| Evolution reasoning | Bisa menambah/mengubah event tanpa merusak consumer lama |
| Failure reasoning | Bisa menangani duplicate, out-of-order, replay, dan late events |
| Governance | Event punya owner, lifecycle, schema, compatibility rule, dan review discipline |
Latihan terbaik bukan membuat event sebanyak mungkin. Latihan terbaik adalah mengambil satu workflow dan bertanya:
“Apa fakta domain yang benar-benar terjadi, siapa yang berhak menyatakannya, dan consumer boleh menyimpulkan apa dari fakta ini?”
2. Event Is Not Just a Message
Message adalah unit transport. Event adalah makna.
Message = sesuatu yang dikirim melalui broker/queue/topic.
Event = fakta yang sudah terjadi dalam domain.
Sebuah message bisa berisi:
- event;
- command;
- query request;
- query response;
- notification;
- state snapshot;
- document transfer;
- heartbeat;
- control signal;
- tombstone.
Jangan semua disebut event hanya karena lewat Kafka.
Jika klasifikasi salah, contract akan salah.
Contoh:
{
"type": "SEND_EMAIL",
"customerId": "cus_123"
}
Ini bukan event. Ini command/task.
Event yang benar:
{
"type": "CustomerEmailVerificationRequested",
"customerId": "cus_123"
}
Atau jika email sudah dikirim:
{
"type": "CustomerEmailVerificationEmailSent",
"customerId": "cus_123",
"emailChannel": "PRIMARY"
}
Makna berbeda. Consumer behavior berbeda.
3. Event as Fact
Event harus merepresentasikan sesuatu yang sudah terjadi.
CustomerRegistered
PaymentAuthorized
CaseSubmitted
AccountFrozen
KycVerificationRejected
ShipmentDispatched
PolicyRuleActivated
Event fact memiliki properties:
- past tense;
- immutable;
- emitted by authority;
- can be stored as history;
- can be replayed;
- can be interpreted without asking producer internal state;
- can be used by multiple consumers for different reasons.
3.1 Good Event Name
CustomerRegistered
CaseApproved
PaymentCaptured
AccountSuspended
KycReviewCompleted
EvidenceAttachedToCase
3.2 Bad Event Name
ProcessCustomer
UpdateDatabase
SendToCRM
DoKyc
CallRiskService
CustomerEvent
StatusChanged
DataChanged
Kenapa buruk?
| Name | Problem |
|---|---|
ProcessCustomer | command/procedure |
UpdateDatabase | implementation detail |
SendToCRM | consumer-specific |
DoKyc | command |
CustomerEvent | terlalu generik |
StatusChanged | status apa, dari apa ke apa, meaning apa? |
DataChanged | tidak memberi domain semantics |
Event name adalah contract. Nama generik membuat consumer harus membaca payload dan menebak.
4. Command vs Event
Command meminta sesuatu terjadi. Event menyatakan sesuatu sudah terjadi.
| Dimension | Command | Event |
|---|---|---|
| Tense | imperative/future | past |
| Coupling | targeted receiver | broadcast/fan-out possible |
| Ownership | requester asks | authority declares |
| Mutability | can be rejected | happened already |
| Consumer behavior | perform action | react/derive/update |
| Example | ApproveCase | CaseApproved |
| Failure | command can fail | event should not be “unhappened” |
| Routing | usually specific handler | many consumers |
4.1 Command Example
{
"commandId": "cmd_01J2X41XAZMQWR61PZ0TBP87GV",
"type": "ApproveCase",
"caseId": "case_01J2X420G91GR8ZJH0ZDR2PKQJ",
"requestedBy": "usr_01J2X42KFTZQ6A2XQD2E7YNDYK",
"reasonCode": "EVIDENCE_COMPLETE"
}
4.2 Event Example
{
"eventId": "evt_01J2X45B2M7G3G4WKEXZSM6D7E",
"type": "CaseApproved",
"caseId": "case_01J2X420G91GR8ZJH0ZDR2PKQJ",
"approvedBy": "usr_01J2X42KFTZQ6A2XQD2E7YNDYK",
"approvedAt": "2026-06-29T03:00:00Z",
"reasonCode": "EVIDENCE_COMPLETE"
}
4.3 Wrong Use: Event as Command
Bad:
{
"eventType": "GenerateInvoice",
"orderId": "ord_123"
}
This asks some service to act. It should be command/task.
Better:
{
"commandType": "GenerateInvoice",
"orderId": "ord_123"
}
or fact event:
{
"eventType": "OrderReadyForInvoicing",
"orderId": "ord_123"
}
Then invoice service reacts.
5. Notification vs Event
Notification says “look over there” or “something relevant happened”, often with minimal detail.
Event says “this fact happened” and should carry enough stable semantics.
5.1 Notification
{
"notificationId": "ntf_123",
"type": "CustomerChanged",
"customerId": "cus_123",
"resourceUrl": "/customers/cus_123"
}
Consumer likely calls API to fetch current state.
5.2 Event
{
"eventId": "evt_123",
"type": "CustomerLifecycleStatusChanged",
"customerId": "cus_123",
"previousLifecycleStatus": "PENDING_REVIEW",
"newLifecycleStatus": "ACTIVE",
"changedAt": "2026-06-29T03:00:00Z",
"reasonCode": "KYC_VERIFIED"
}
Consumer can update projection without immediate API call.
5.3 Trade-off
| Approach | Pros | Cons |
|---|---|---|
| Notification | small, low schema coupling, current state fetched | extra API calls, race conditions, provider load |
| Rich event | autonomous consumers, replay-friendly, lower API dependency | schema ownership, compatibility burden |
| State transfer | projection building easy | larger payload, privacy/data governance burden |
| Minimal event | less leakage | consumer may need provider call |
Decision:
- If consumers need to build projections, event needs enough data.
- If data is sensitive or changes rapidly, notification + fetch may be safer.
- If strong audit/replay is needed, event should be fact-rich.
- If consumer-specific data needs vary widely, avoid stuffing everything into one event.
6. State Transfer Event vs Domain Event
6.1 Domain Event
Domain event focuses on what happened.
{
"type": "CustomerEmailChanged",
"customerId": "cus_123",
"previousEmailAddress": "old@example.com",
"newEmailAddress": "new@example.com",
"changedAt": "2026-06-29T03:10:00Z"
}
Good for workflow/audit/reaction.
6.2 State Transfer Event
State transfer event sends current state snapshot.
{
"type": "CustomerSnapshotUpdated",
"customerId": "cus_123",
"snapshotVersion": 42,
"customer": {
"displayName": "Ayu Lestari",
"lifecycleStatus": "ACTIVE",
"kycStatus": "VERIFIED",
"emailAddress": "new@example.com"
}
}
Good for projection replication/search/read models.
6.3 Difference
| Dimension | Domain Event | State Transfer |
|---|---|---|
| Meaning | Something happened | Current state is X |
| Size | usually smaller | usually larger |
| Replay | reconstruct history | reconstruct latest projection |
| Consumer logic | reacts to facts | updates local view |
| Ordering sensitivity | high for same aggregate | high but version helps |
| Privacy risk | selective | higher |
| Compatibility | event-type-specific | snapshot schema evolves |
| Example | CaseApproved | CaseSnapshotUpdated |
6.4 Hybrid Event
Sometimes useful:
{
"type": "CustomerLifecycleStatusChanged",
"customerId": "cus_123",
"previousLifecycleStatus": "PENDING_REVIEW",
"newLifecycleStatus": "ACTIVE",
"customerVersion": 42,
"customerSnapshot": {
"displayName": "Ayu Lestari",
"lifecycleStatus": "ACTIVE",
"kycStatus": "VERIFIED"
}
}
Trade-off: easy for consumers but heavier and more coupled.
Rule:
Do not mix domain event and snapshot blindly. Make the contract explicit about whether payload is fact, state, or both.
7. Integration Event vs Internal Domain Event
Internal domain event:
public record CustomerVerified(
CustomerId customerId,
VerificationDecision decision,
Instant occurredAt
) {}
Integration event:
{
"eventId": "evt_01J2X5GV4SKVW9ED5JFK4V45WA",
"eventType": "CustomerKycVerified",
"customerId": "cus_01J2X5J6SY9DGYD0B95WRNXZ2E",
"verifiedAt": "2026-06-29T03:20:00Z",
"verificationLevel": "STANDARD"
}
Do not publish internal domain event class directly.
Reasons:
- internal model changes faster;
- internal value objects may leak implementation;
- internal event may contain sensitive fields;
- internal naming may not be consumer-facing;
- internal event may not include integration metadata;
- integration event needs compatibility lifecycle;
- internal event may be too granular or too broad.
Pattern:
Mapper is a contract boundary.
8. Event Authority
An event must have an authority: the system that is allowed to state that fact.
Examples:
| Event | Authority |
|---|---|
CustomerRegistered | Customer service |
PaymentAuthorized | Payment service/provider adapter |
CaseApproved | Case management service |
KycVerificationCompleted | KYC service |
AccountFrozen | Account service |
PolicyRulePublished | Policy/rules service |
Anti-pattern:
CRM service emits PaymentAuthorized
Unless CRM is truly the payment authority, this is wrong.
Why authority matters:
- consumer trust;
- conflict resolution;
- data ownership;
- audit;
- replay correctness;
- governance approval;
- lineage.
If two services can emit the same event type, contract must define authority and dedup/conflict semantics explicitly.
9. Event Identity
Every integration event should have stable identity.
{
"eventId": "evt_01J2X6RKHNQME8YGR5K5BA12H4"
}
Event ID is not always the same as:
| ID | Meaning |
|---|---|
eventId | unique event occurrence |
aggregateId | entity/aggregate whose state changed |
commandId | command that caused event |
correlationId | end-to-end business/process correlation |
causationId | immediate parent event/command |
traceId | distributed tracing technical correlation |
messageId | broker/message system ID |
schemaId | schema registry artifact/version ID |
9.1 Example
{
"eventId": "evt_01J2X6RKHNQME8YGR5K5BA12H4",
"eventType": "CaseApproved",
"aggregateType": "Case",
"aggregateId": "case_01J2X6VD7MWK23YWGQ5KTV8DGY",
"commandId": "cmd_01J2X6WBHQ4T428WHM4EFSCKS2",
"correlationId": "corr_01J2X6XJH5Y8KX674M4CY5BSYX",
"causationId": "evt_01J2X6Z6X0R2F7H2Y8VDACDJ21",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}
9.2 Event ID Requirements
Event ID should be:
- globally unique or unique within event stream;
- immutable;
- stable across retry/publish retry;
- included in logs/metrics/traces;
- used for idempotent consumer deduplication;
- not derived from payload hash unless policy clear;
- not broker offset alone if replay/migration across topics is possible.
10. Aggregate Identity
For stateful domain systems, event usually relates to an aggregate.
{
"aggregateType": "Case",
"aggregateId": "case_01J2X6VD7MWK23YWGQ5KTV8DGY",
"aggregateVersion": 17
}
Why important?
- ordering per aggregate;
- optimistic projection updates;
- replay;
- idempotency;
- deduplication;
- audit history;
- consumer partitioning;
- state machine validation.
10.1 Aggregate Version
aggregateVersion or sequenceNumber helps detect missing/out-of-order events.
Example:
{
"eventType": "CaseApproved",
"caseId": "case_123",
"caseVersion": 17
}
Consumer projection:
if (event.caseVersion() <= projection.version()) {
return; // duplicate or old event
}
if (event.caseVersion() != projection.version() + 1) {
quarantine(event); // missing event or out-of-order
}
This is critical for strict projections.
11. Correlation vs Causation vs Trace
These are often confused.
11.1 Correlation ID
Groups work belonging to same business/process flow.
Example:
customer onboarding flow
Many commands/events share one correlationId.
11.2 Causation ID
Points to immediate cause.
Example:
KycVerificationCompleted caused CustomerActivated
causationId may be event ID or command ID.
11.3 Trace ID
Technical distributed trace ID used by observability systems.
May cross HTTP calls and message processing.
11.4 Diagram
11.5 Contract Rule
- Use
correlationIdfor journey/process. - Use
causationIdfor immediate parent. - Use
traceIdfor distributed tracing. - Do not make one field serve all three roles.
12. Time Semantics
Event time fields must be explicit.
| Field | Meaning |
|---|---|
occurredAt | business fact happened |
decidedAt | decision was made |
effectiveAt | state/rule becomes effective |
publishedAt | producer published event |
receivedAt | broker/gateway received event |
processedAt | consumer processed event |
ingestedAt | data platform ingested event |
recordedAt | event persisted in outbox/event store |
12.1 Example
{
"eventType": "PolicyRuleActivated",
"occurredAt": "2026-06-29T03:30:00Z",
"effectiveAt": "2026-07-01T00:00:00Z",
"publishedAt": "2026-06-29T03:30:02Z"
}
Do not collapse all into timestamp.
12.2 Late Events
If event arrives late:
occurredAt = 10:00
publishedAt = 10:30
processedAt = 10:31
Consumer must know whether to process based on occurrence time or processing time.
Examples:
| Use case | Time basis |
|---|---|
| audit history | occurredAt |
| SLA of broker | publishedAt/receivedAt |
| projection freshness | processedAt |
| regulatory effective rule | effectiveAt |
| workflow timeout | occurredAt or command acceptedAt depending contract |
13. Event Immutability
Event should not be updated after publication.
If correction is needed, publish compensating/correction event.
Bad:
Update old event payload in topic/storage
Better:
CustomerEmailChanged
CustomerEmailChangeCorrected
or:
CaseApprovalRevoked
depending domain meaning.
Why immutability matters:
- replay determinism;
- audit;
- consumer idempotency;
- lineage;
- legal/regulatory evidence;
- debugging;
- event store correctness.
13.1 Correction Event Example
{
"eventType": "CustomerBirthDateCorrectionApplied",
"customerId": "cus_123",
"previousBirthDate": "1994-05-18",
"correctedBirthDate": "1994-05-19",
"correctionReason": "DOCUMENT_REVIEW",
"correctedAt": "2026-06-29T03:40:00Z",
"correctedBy": "usr_123"
}
This is explicit and auditable.
14. Event Granularity
Granularity choices:
14.1 Coarse Event
CustomerUpdated
Pros:
- fewer event types;
- easier producer management.
Cons:
- consumer must inspect fields;
- unclear semantics;
- hard to route;
- hard to test;
- high coupling;
- schema grows.
14.2 Fine-Grained Event
CustomerEmailChanged
CustomerPhoneNumberChanged
CustomerKycStatusChanged
CustomerLifecycleStatusChanged
Pros:
- explicit semantics;
- easier consumer filtering;
- clearer compatibility;
- better audit.
Cons:
- more event types;
- higher governance overhead;
- risk of event explosion.
14.3 Decision
Use domain significance, not field-level reflex.
Good event type if:
- consumer actions differ;
- audit meaning differs;
- business rule differs;
- lifecycle differs;
- security/data classification differs;
- ordering/replay semantics differ.
Do not create event type for every column update unless each has domain meaning.
15. Event Naming Pattern
Recommended:
<Noun><PastTenseVerb>
Examples:
CustomerRegistered
CustomerActivated
CaseSubmitted
CaseApproved
PaymentAuthorized
PaymentCaptured
AccountFrozen
PolicyRulePublished
EvidenceAttached
Sometimes:
<Noun><State>Changed
Examples:
CustomerLifecycleStatusChanged
CaseAssignmentChanged
AccountLimitChanged
But “changed” must include previous/new state or reason.
Bad:
CustomerChanged
StatusChanged
UpdateHappened
NotificationEvent
15.1 Include Domain Scope
If Approved can refer to many domains:
CaseApproved
LoanApplicationApproved
PaymentApproved
PolicyExceptionApproved
Avoid global ambiguous event names.
16. Payload Design: Minimal but Sufficient
Event payload must be enough for intended consumer classes.
Too little:
{
"eventType": "CustomerActivated",
"customerId": "cus_123"
}
Consumer must call API, causing coupling and race.
Too much:
{
"eventType": "CustomerActivated",
"customer": {
"...": "entire customer entity with sensitive fields"
}
}
Privacy and schema coupling.
Balanced:
{
"eventType": "CustomerActivated",
"customerId": "cus_123",
"previousLifecycleStatus": "PENDING_REVIEW",
"newLifecycleStatus": "ACTIVE",
"activatedAt": "2026-06-29T03:50:00Z",
"activationReason": "KYC_VERIFIED"
}
Decision checklist:
- What will common consumers do?
- Can they act without API call?
- Is data sensitive?
- Is field stable?
- Is it authoritative?
- Is it needed for replay?
- Can it be derived safely by consumer?
- Is it consumer-specific?
- Does it create privacy/data minimization problem?
- Is it part of long-term contract?
17. Event Ordering
Event contract must define ordering assumptions.
Possible guarantees:
| Level | Meaning |
|---|---|
| None | consumers must handle arbitrary order |
| Per topic partition | broker-specific partition order |
| Per aggregate | events for same aggregate ordered |
| Global order | rarely practical |
| Sequence-number based | consumer can detect gaps |
| Version-based | aggregateVersion increments |
Example contract:
ordering:
guarantee: per-aggregate
partitionKey: caseId
sequenceField: caseVersion
duplicateDelivery: possible
outOfOrderAcrossAggregates: allowed
Do not imply global ordering unless you can provide it.
17.1 Out-of-Order Example
Events:
CaseApproved version=5
CaseSubmitted version=4
Consumer receives approved first. What should happen?
Options:
- buffer/quarantine;
- fetch current state;
- apply last-write-wins if state transfer;
- ignore old event if version lower;
- fail and alert;
- process if operation commutative.
Contract must guide this.
18. Duplicate Delivery
Most distributed messaging systems require consumers to tolerate duplicates.
Event contract should include stable event ID.
Consumer pattern:
public void handle(CaseApprovedEvent event) {
if (processedEventRepository.exists(event.eventId())) {
return;
}
process(event);
processedEventRepository.markProcessed(event.eventId());
}
But dedup storage has trade-offs:
- retention duration;
- storage size;
- transactional boundary;
- consumer group specific;
- replay semantics.
Event ID is necessary but not sufficient. Consumer must define dedup strategy.
19. Replay Semantics
Can old events be replayed?
Contract should specify:
| Question | Why it matters |
|---|---|
| Is replay supported? | consumer projection rebuild |
| Are events immutable? | replay determinism |
| Are schemas still readable? | compatibility |
| Are old events semantically valid? | business rule drift |
| Are tombstones included? | deletion handling |
| Are corrections represented? | audit |
| Are side-effect consumers protected? | avoid sending emails again |
19.1 Replay-Safe vs Side-Effect Consumer
Projection consumer:
safe to replay
Email sender:
not replay-safe unless idempotent by eventId/business key
Contract should warn:
replay:
supported: true
consumerRequirement: Consumers causing external side effects must deduplicate by eventId.
20. Event Compatibility
Schema compatibility is not enough.
20.1 Structurally Compatible but Semantically Breaking
Old event:
{
"eventType": "CustomerActivated",
"customerId": "cus_123"
}
Meaning old:
customer may transact
Meaning new:
customer profile activated, but transaction access may still be blocked
Schema unchanged. Consumers break.
20.2 Safe Event Evolution
Usually safe:
- add optional field;
- add metadata field;
- add new event type;
- add optional nested object;
- widen field constraint;
- add reasonCode if optional;
- add new consumer-independent header.
Dangerous:
- add enum value;
- make optional field required;
- change event meaning;
- change ordering key;
- change partition key;
- change event authority;
- change replay behavior;
- change time semantics;
- change id format;
- change null/absent semantics;
- change topic.
Breaking:
- remove field;
- rename field;
- change type;
- change event type name;
- change key field;
- change payload from fact to snapshot without compatibility;
- stop publishing event;
- publish event from different authority without migration.
21. Event Type Lifecycle
21.1 Proposed
Design only.
21.2 Experimental
Limited consumer. Must have scope and expiry.
21.3 Stable
Public integration event. Requires compatibility discipline.
21.4 Deprecated
Still published but new consumers should avoid.
21.5 Retired
No longer published. Requires migration and consumer inventory.
Event retirement is often harder than API endpoint retirement because consumers may be passive and less visible.
22. Event Consumer Contract
A good event contract should say what consumers may assume.
Example:
eventType: CaseApproved
consumerAssumptions:
- The case existed before this event.
- The case state transitioned to APPROVED.
- The approving actor had required authority at decision time.
- The event is immutable.
- Duplicate delivery is possible.
- Ordering is guaranteed per caseId when consumed from the case-events topic with key=caseId.
- Consumers must tolerate unknown optional fields.
- Consumers must not assume global ordering across cases.
This is more valuable than schema alone.
23. Event Producer Contract
Producer promises:
- event is emitted after durable state change;
- event ID stable across publish retries;
- event type name stable;
- schema registered before publish;
- event key follows contract;
- event ordering follows contract;
- event does not include forbidden sensitive data;
- event timestamp semantics correct;
- event is published at-least-once or exactly-once depending stated guarantee;
- deprecation policy followed.
23.1 Outbox Pattern Context
Typical reliable publish:
This reduces risk of state change without event or event without state change.
24. Event Consumer Responsibility
Consumer must not assume exactly-once processing unless contract explicitly gives it and implementation actually supports it.
Consumer responsibilities:
- idempotent processing;
- duplicate tolerance;
- out-of-order handling;
- schema compatibility;
- unknown field tolerance;
- unknown enum strategy;
- DLQ/quarantine;
- backpressure handling;
- replay safety;
- observability;
- poison message strategy;
- version migration.
Event-driven architecture shifts responsibility to both producer and consumer.
25. Event Contract Documentation Template
eventType: CaseApproved
description: A case has been approved by an authorized actor.
ownerTeam: case-management-platform
authority: case-service
lifecycle: stable
topic: case-events
messageKey: caseId
ordering:
guarantee: per-case
sequenceField: caseVersion
delivery:
guarantee: at-least-once
duplicates: possible
replay:
supported: true
sideEffectWarning: Consumers must deduplicate external side effects by eventId.
schema:
format: avro
subject: case.CaseApproved
compatibility:
mode: backward-transitive
identity:
eventId: globally unique event occurrence
aggregateId: caseId
aggregateVersion: caseVersion
time:
occurredAt: approval decision time
publishedAt: broker publish time
consumerAssumptions:
- Case state is APPROVED after this event.
- Approval may be reversed only by a later CaseApprovalRevoked event.
security:
dataClassification: confidential
containsPII: false
This is the level of documentation expected in mature platform teams.
26. Java Event Type Modelling
26.1 Internal Domain Event
public record CaseApprovedDomainEvent(
CaseId caseId,
CaseVersion caseVersion,
UserId approvedBy,
ApprovalReason reason,
Instant approvedAt
) {}
26.2 Integration Event
public record CaseApprovedEvent(
String eventId,
String eventType,
String aggregateType,
String aggregateId,
long aggregateVersion,
String correlationId,
String causationId,
Instant occurredAt,
Instant publishedAt,
Payload payload
) {
public record Payload(
String caseId,
long caseVersion,
String approvedBy,
String reasonCode,
Instant approvedAt
) {}
}
26.3 Mapper
public final class CaseEventMapper {
public CaseApprovedEvent toIntegrationEvent(
CaseApprovedDomainEvent domainEvent,
EventContext context
) {
Instant now = context.clock().instant();
return new CaseApprovedEvent(
context.eventId(),
"CaseApproved",
"Case",
domainEvent.caseId().value(),
domainEvent.caseVersion().value(),
context.correlationId(),
context.causationId(),
domainEvent.approvedAt(),
now,
new CaseApprovedEvent.Payload(
domainEvent.caseId().value(),
domainEvent.caseVersion().value(),
domainEvent.approvedBy().value(),
domainEvent.reason().code(),
domainEvent.approvedAt()
)
);
}
}
Note:
- integration event has metadata;
- payload has domain data;
- internal value objects mapped to stable external strings;
- no entity leakage.
27. Event Contract Review Checklist
27.1 Classification
- Is this truly an event/fact?
- Is it actually a command?
- Is it notification or state transfer?
- Is the event name past tense and domain-specific?
- Is authority clear?
27.2 Semantics
- What exactly happened?
- What may consumer assume?
- What should consumer not assume?
- Is event immutable?
- Is correction/compensation model clear?
- Is state transition clear?
27.3 Identity and Time
- Is eventId present?
- Is aggregateId present if relevant?
- Is aggregateVersion/sequence present?
- Are correlationId and causationId clear?
- Are occurredAt/publishedAt semantics clear?
27.4 Delivery and Ordering
- Is delivery guarantee stated?
- Are duplicates possible?
- Is ordering guarantee stated?
- Is partition/message key stated?
- Is replay supported?
- Are side-effect consumer warnings documented?
27.5 Schema and Payload
- Does payload include enough stable data?
- Does it avoid internal model leakage?
- Does it avoid sensitive data?
- Are enum evolution rules clear?
- Are nullable/optional semantics clear?
- Is schema registered/governed?
27.6 Compatibility
- Is adding this event type safe for consumers?
- Does it replace/deprecate another event?
- Are existing consumers impacted?
- Is topic/key changing?
- Is semantic meaning stable?
28. Event Anti-Patterns
28.1 Event as RPC
UserCreated event consumed only by one service to perform required synchronous action
If producer depends on immediate consumer success, it may be command/RPC, not event.
28.2 Generic DataChanged Event
{
"eventType": "DataChanged",
"table": "customer",
"id": "123"
}
This leaks database thinking and lacks domain semantics.
28.3 Consumer-Specific Event
SendCustomerToCRM
This couples producer to one consumer. Prefer domain fact and let CRM consume.
28.4 Entity Dump Event
Publishing entire database entity.
Problems:
- privacy leakage;
- internal coupling;
- schema instability;
- consumer misuse;
- unnecessary size.
28.5 No Event ID
Consumers cannot deduplicate.
28.6 Timestamp Ambiguity
Field timestamp without meaning.
28.7 No Ordering Contract
Consumers assume ordering that producer never promised.
28.8 Reusing Event Type with New Meaning
Silent semantic break.
28.9 Publishing Before Commit
Consumer sees fact that might roll back.
28.10 Side Effects on Replay
Consumer sends duplicate emails/payments during replay.
29. Practice Lab
Lab 1 — Classify Messages
Classify as event, command, notification, or state transfer:
ApproveCaseCaseApprovedCustomerChangedCustomerSnapshotUpdatedSendWelcomeEmailPaymentAuthorizationRequestedPaymentAuthorizedPolicyRuleActivatedRefreshCustomerProjectionDocumentUploaded
For each, explain whether name/payload should change.
Lab 2 — Design Event Contract
Workflow:
A case is submitted, reviewed, approved, possibly reopened.
Design event types with:
- event name;
- authority;
- aggregate ID;
- version;
- occurredAt;
- causation/correlation;
- payload fields;
- consumer assumptions.
Lab 3 — Fix Bad Event
Input:
{
"eventType": "CustomerUpdated",
"id": 123,
"status": "A",
"timestamp": "2026-06-29T10:00:00"
}
Refactor into a proper event contract.
Lab 4 — Ordering Strategy
Given events:
CaseSubmitted version=3
CaseApproved version=5
CaseAssigned version=4
Design consumer handling for:
- strict projection;
- notification-only consumer;
- audit log consumer.
Lab 5 — Replay Safety
Consumer sends SMS when CustomerRegistered is received. Design idempotency/replay protection.
30. Senior Engineer Heuristics
- Event is a fact, not a function call.
- Message transport does not define event semantics.
- Past-tense naming prevents many design mistakes.
- Authority matters: only the owner of a fact should publish it.
- Event ID is mandatory for serious integration events.
- Correlation, causation, and trace are different things.
timestampis not enough; name the time semantics.- Domain event and integration event should not be the same object by default.
- Generic
Changedevents are usually design debt. - Snapshot events and domain events solve different problems.
- Replay is a contract, not just broker capability.
- Consumers must be idempotent unless impossibility is explicitly guaranteed.
- Semantic compatibility matters more than schema compatibility.
- A good event tells consumers what they may assume.
- If an event has only one required consumer and producer depends on it immediately, question whether it is really an event.
31. Summary
Event contract engineering starts with mental model, not schema. An event is a domain fact declared by an authority. A message can carry an event, command, notification, or state transfer, and each has different contract semantics.
Main takeaways:
- event is not the same as message;
- event should represent something that already happened;
- command asks for action, event declares fact;
- notification points to change, rich event carries fact;
- state transfer event carries current projection state;
- internal domain events should be mapped to stable integration events;
- event identity, aggregate identity, correlation, causation, and trace must be separated;
- time fields need explicit meaning;
- ordering, duplicate delivery, and replay are part of contract;
- semantic compatibility is more important than schema compatibility.
Part berikutnya membahas event envelope design: bagaimana metadata, payload, routing, idempotency, tracing, tenancy, classification, and schema versioning disusun menjadi envelope yang stabil dan governable.
You just completed lesson 13 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.