Back to blog

Designing event payloads: what should be in an event?

Backend Architecture Notes: an event is a contract, design it like one


In event-driven systems, publishing an event is not only about sending a message.

The shape of the event matters.

A good event payload makes the system easier to understand, consume, debug, version, replay, and operate.

A bad event payload creates confusion. Consumers need to call back to the producer for missing data. Events become tightly coupled to database tables. Important identifiers are missing. Debugging requires searching through multiple systems just to understand what happened.

Event design is architecture design.

An event is a contract between the service that publishes a fact and the systems that react to it.

So the question is:

What should be inside an event?

Start with the business fact

A good event starts with a clear business fact.

For example:

OrderCreated
PaymentAuthorized
InventoryReserved
SubscriptionCancelled
InvoiceGenerated
GameRoundCompleted
BonusGranted

The event name should describe something meaningful that happened in the domain.

It should not describe an implementation detail.

For example, this is probably too technical:

OrderRowInserted
DatabaseRecordUpdated
PaymentTableChanged
CacheInvalidated

Those may be useful internally, but they are not great domain events.

A consumer should be able to understand the event without knowing the producer's database schema.

A good event says:

Something meaningful happened in the business.

Not:

Something changed in a table.

Use past tense

Events should usually be named in the past tense.

For example:

OrderCreated
PaymentSucceeded
SubscriptionCancelled
InventoryReserved

That makes it clear that the event is a fact.

It already happened.

Compare that with:

CreateOrder
ChargePayment
CancelSubscription
ReserveInventory

Those are commands. They ask for something to happen.

The naming convention helps everyone understand how the message should be handled.

A command can be accepted or rejected.

An event should be treated as a fact that already occurred.

Separate metadata from payload

I like events that have a clear envelope.

The envelope contains metadata about the event.

The payload contains the business data.

For example:

{
  "eventId": "evt_123",
  "eventType": "OrderCreated",
  "eventVersion": 1,
  "occurredAt": "2026-06-23T10:00:00Z",
  "publishedAt": "2026-06-23T10:00:01Z",
  "producer": "order-service",
  "correlationId": "corr_456",
  "causationId": "cmd_789",
  "payload": {
    "orderId": "order_123",
    "customerId": "customer_456",
    "totalAmount": 49.99,
    "currency": "EUR"
  }
}

This structure keeps technical event information separate from business data.

That makes events easier to log, trace, validate, and evolve.

Important metadata fields

There are a few metadata fields I almost always want.

eventId
eventType
eventVersion
occurredAt
producer
correlationId
causationId

Depending on the system, I may also include:

publishedAt
tenantId
schemaId
traceId
environment
producerVersion

Not every system needs all of these.

But missing metadata often becomes painful later.

It is much easier to include useful metadata from the start than to add it after the first production incident.

eventId

Every event should have a unique event ID.

For example:

eventId = evt_123

This ID represents one specific event message.

It is useful for:

Logging
Tracing
Deduplication
Dead-letter queue inspection
Replay
Auditing
Debugging

If a consumer fails, you want to search for the exact event that caused the failure.

Without an event ID, debugging becomes harder.

For example, if a dead-letter queue contains a failed message, the event ID gives you a handle for investigation.

eventType

The event type describes what happened.

For example:

OrderCreated
PaymentAuthorized
SubscriptionCancelled

This field is important because consumers often route events based on type.

It should be stable, meaningful, and domain-oriented.

Avoid vague event types like:

Updated
Changed
Processed
Completed

These names do not say enough.

What was updated?

What changed?

What was completed?

A good event type should communicate the business fact clearly.

eventVersion

The event version tells consumers which schema version they are receiving.

For example:

eventVersion = 1

This is important because event schemas evolve.

A consumer may need to handle:

OrderCreated version 1
OrderCreated version 2
OrderCreated version 3

Versioning makes schema changes explicit.

Without an event version, consumers have to guess which shape they received.

That usually leads to fragile code.

occurredAt and publishedAt

I like to distinguish between when the business event occurred and when the message was published.

For example:

occurredAt = when the business fact happened
publishedAt = when the event was published to the broker

Usually these are close together.

But they can differ.

For example, with the Outbox pattern, the event may be recorded during the database transaction and published a few seconds later.

Order was created at 10:00:00
Event was published at 10:00:05

Both timestamps can be useful.

occurredAt is useful for business timelines.

publishedAt is useful for operational latency.

For example, the difference between them can show outbox delay.

producer

The producer tells you which service published the event.

For example:

producer = order-service

This is useful when debugging.

If an event has an invalid payload, unsupported version, or unexpected value, you need to know where it came from.

In larger systems, you may also include the producer version:

producerVersion = 2026.06.23.1

That can help connect an event issue to a deployment.

correlationId

A correlation ID connects all events, commands, logs, and traces that belong to the same business flow.

For example, an order flow may include:

CreateOrder request
OrderCreated event
ReserveInventory command
InventoryReserved event
AuthorizePayment command
PaymentAuthorized event
OrderConfirmed event

All of these should share the same correlation ID.

That lets you follow the whole process across services.

A simple way to think about it:

correlationId = the whole story

When a user says an order is stuck, the correlation ID helps you reconstruct the story.

causationId

A causation ID tells you what caused this event.

For example:

ReserveInventory command caused InventoryReserved event
InventoryReserved event caused AuthorizePayment command
AuthorizePayment command caused PaymentAuthorized event

The causation ID links one step to the previous step.

A simple way to think about it:

causationId = the previous chapter

This is especially useful in sagas and long-running workflows.

It helps you understand why an event exists.

Business identifiers

Metadata helps with tracing.

Business identifiers help with understanding.

For example:

orderId
paymentId
customerId
accountId
subscriptionId
invoiceId
reservationId
playerId
gameRoundId

These identifiers should usually be in the payload or metadata, depending on how the system is designed.

If an event is about an order, include the orderId.

If it is about a payment, include the paymentId.

If it belongs to a tenant, include the tenantId.

Consumers, logs, dashboards, and support tools all need these identifiers.

A message without business identifiers is much harder to operate.

How much data should an event contain?

This is one of the most important design questions.

Should an event contain only IDs?

Or should it contain a larger snapshot of relevant data?

For example, should OrderCreated look like this?

{
  "orderId": "order_123"
}

Or like this?

{
  "orderId": "order_123",
  "customerId": "customer_456",
  "totalAmount": 49.99,
  "currency": "EUR",
  "items": [
    {
      "productId": "product_1",
      "quantity": 2,
      "unitPrice": 19.99
    }
  ]
}

There is no universal answer.

It depends on what the event is for.

But there are trade-offs.

Thin events

A thin event contains mostly identifiers.

For example:

{
  "eventType": "OrderCreated",
  "orderId": "order_123"
}

The benefit is that the event is small and simple.

The downside is that consumers may need to call the producer to get more information.

For example, the Email Service may receive OrderCreated, but then it needs to call the Order Service to get order details.

That creates runtime coupling.

If many consumers call back to the producer, the producer can become a bottleneck.

Thin events can be fine when consumers only need the ID, or when the producer intentionally wants consumers to fetch current state.

But thin events are not automatically better.

They can move complexity from the event payload into synchronous service calls.

Rich events

A rich event contains the information consumers commonly need.

For example:

{
  "eventType": "OrderCreated",
  "orderId": "order_123",
  "customerId": "customer_456",
  "totalAmount": 49.99,
  "currency": "EUR",
  "items": [
    {
      "productId": "product_1",
      "quantity": 2,
      "unitPrice": 19.99
    }
  ]
}

The benefit is that consumers can process the event without calling back to the producer.

This improves decoupling and resilience.

The downside is that the event schema becomes larger and more important.

More fields mean more schema evolution concerns.

Also, if you include too much data, you may expose information consumers do not need.

A good rich event includes enough data to represent the business fact and support likely consumers, but not every internal detail.

Event-carried state transfer

Rich events are sometimes called event-carried state transfer.

The idea is that the event carries enough state for consumers to update their own views.

For example, a Reporting Service can build a projection from OrderCreated, PaymentAuthorized, and OrderCancelled events without constantly calling the Order Service.

This can be very useful.

It helps consumers remain independent.

But it also means the event contract matters more.

The producer must be careful about field meanings, versioning, and backward compatibility.

Do not include everything

A rich event does not mean dumping the entire database row into the event.

Avoid events like this:

{
  "eventType": "OrderCreated",
  "payload": {
    "all_database_columns": "...",
    "internal_flags": "...",
    "temporary_state": "...",
    "debug_values": "..."
  }
}

That couples consumers to internal implementation details.

It also increases the chance of leaking sensitive or irrelevant data.

An event should contain information that belongs to the business fact.

Not every field the producer happens to store.

Good event design is selective.

Do not expose internal database models

This is worth repeating.

Do not treat your event as a public version of your database table.

Database schemas change for internal reasons.

Events are integration contracts.

If you expose internal models, consumers become coupled to your storage design.

For example, this is not ideal:

{
  "table": "orders",
  "operation": "insert",
  "row": {
    "id": "order_123",
    "cust_fk": "customer_456",
    "amt_net": 41.31,
    "amt_tax": 8.68,
    "st": "P"
  }
}

A better event is domain-oriented:

{
  "eventType": "OrderCreated",
  "orderId": "order_123",
  "customerId": "customer_456",
  "status": "PendingPayment",
  "totalAmount": 49.99,
  "currency": "EUR"
}

Consumers should not need to know that the database column is called cust_fk.

Include enough information for idempotency

Events should include identifiers that allow consumers to process them safely.

For example, a payment event should include:

paymentId
orderId
amount
currency

A bonus event should include:

bonusGrantId
playerId
amount
reason

An inventory event should include:

reservationId
orderId
productId
quantity

The consumer needs a stable business key to detect duplicate processing.

For example:

Apply payment pay_123 once
Grant bonus bonus_456 once
Reserve inventory reservation_789 once

If the event does not include the right business key, idempotency becomes harder.

Include enough information for ordering

If ordering matters, include a version or sequence number.

For example:

{
  "eventType": "AccountBalanceUpdated",
  "accountId": "account_123",
  "version": 17,
  "balance": 150.00,
  "currency": "EUR"
}

The consumer can then detect stale events.

For example:

Current version: 17
Incoming version: 16
Ignore as stale

Or detect missing events:

Current version: 17
Incoming version: 19
Version 18 is missing

Without a version, the consumer may not know that it processed events in the wrong order.

Be careful with personal or sensitive data

Events often travel further than expected.

They may be stored in brokers, logs, archives, replay systems, data lakes, and monitoring tools.

That means sensitive data needs care.

Avoid including data unless consumers genuinely need it.

Examples to think carefully about:

Email addresses
Phone numbers
Addresses
Payment details
Identity documents
Personal attributes
Authentication tokens
Session tokens
Internal secrets

Some data should never be in an event.

Some data should be referenced by ID instead.

Some data may need encryption, masking, retention rules, or access controls.

The fact that an event is internal does not mean it is safe to put everything in it.

Avoid derived fields unless useful

Sometimes an event contains derived values.

For example:

totalAmount
taxAmount
discountAmount
netAmount

This can be useful because consumers do not have to recalculate business logic.

But it also means the producer owns the calculation.

That can be good.

For example, a Payment Service should probably publish the final authorized amount rather than forcing every consumer to recalculate it.

But be careful with derived fields that can become inconsistent or ambiguous.

If you include them, document what they mean.

For example:

Does totalAmount include tax?
Does it include discounts?
Is it before or after fees?
Which currency is it in?

Field meaning matters as much as field type.

Avoid ambiguous names

Event fields should be boring and clear.

Bad:

amount
status
date
type
value
data

Better:

totalAmount
paymentStatus
occurredAt
paymentMethodType
bonusValue

Short names can be fine when the context is obvious, but ambiguous names cause problems.

For example, amount in a payment event could mean:

Gross amount
Net amount
Authorized amount
Captured amount
Refunded amount
Fee amount

If the meaning matters, name it clearly.

Future consumers should not have to read producer code to understand the event.

Use explicit units and currencies

For numeric values, include units.

For money, include currency.

Bad:

{
  "amount": 49.99
}

Better:

{
  "amount": 49.99,
  "currency": "EUR"
}

For duration:

{
  "durationMs": 350
}

For distance:

{
  "distanceMeters": 1200
}

For percentages, be clear whether the value is:

0.15
15
15%

These details avoid consumer bugs.

Prefer stable identifiers over display names

Use stable IDs for references.

For example:

{
  "customerId": "customer_123",
  "productId": "product_456"
}

Display names can change.

For example:

Product name
Customer name
Casino display name
Game title

Sometimes including display names is useful for projections or emails.

But the stable identifier should still be present.

A consumer should not have to identify a product by name.

Names are for humans.

IDs are for systems.

Make optional fields explicit

Optional fields are normal.

But consumers need to know which fields are required and which are optional.

For example:

{
  "eventType": "PaymentFailed",
  "paymentId": "pay_123",
  "failureReason": "CardDeclined",
  "providerErrorCode": null
}

If providerErrorCode is optional, document that.

Do not make consumers guess.

Also be careful with the difference between:

Field missing
Field present with null
Field present with empty string

These may have different meanings.

If the difference matters, define it.

Include reason fields for important state changes

When an event represents a failure, cancellation, rejection, or manual intervention, include a reason.

For example:

{
  "eventType": "PaymentFailed",
  "paymentId": "pay_123",
  "orderId": "order_456",
  "failureReason": "CardDeclined"
}

or:

{
  "eventType": "OrderCancelled",
  "orderId": "order_456",
  "cancelledBy": "system",
  "cancellationReason": "PaymentFailed"
}

This is useful for support, analytics, retries, and business reporting.

A state change without a reason often leads to extra investigation later.

Make the source of truth clear

An event should be published by the service that owns the fact.

Order Service publishes OrderCreated
Payment Service publishes PaymentAuthorized
Inventory Service publishes InventoryReserved
Billing Service publishes InvoiceGenerated

This ownership matters.

If multiple services publish events about the same fact, consumers may not know which event to trust.

For example, should the Order Service publish PaymentSucceeded?

Usually no.

The Payment Service owns payment state.

The Order Service may publish OrderMarkedAsPaid, but that is a different fact.

Clear ownership prevents confusing event contracts.

Snapshot event or delta event?

Some events describe a full state.

Others describe a change.

A snapshot-style event:

{
  "eventType": "UserProfileUpdated",
  "userId": "user_123",
  "profile": {
    "displayName": "Jeroen",
    "locale": "nl-BE"
  },
  "version": 8
}

A delta-style event:

{
  "eventType": "UserDisplayNameChanged",
  "userId": "user_123",
  "oldDisplayName": "J.",
  "newDisplayName": "Jeroen",
  "version": 8
}

Both can be valid.

Snapshot events are often easier for projections because the consumer can overwrite current state.

Delta events are more precise and can describe exactly what changed.

The choice depends on the use case.

For many integration events, a small snapshot of the relevant state is easier to consume.

For audit or event sourcing, more specific change events may be better.

Avoid making consumers call back for everything

If every consumer needs to call the producer after receiving an event, the system may not be as decoupled as it looks.

For example:

OrderCreated only contains orderId
Email Service calls Order Service for details
Analytics Service calls Order Service for details
CRM Service calls Order Service for details
Reporting Service calls Order Service for details

Now the Order Service becomes a runtime dependency for all consumers.

If the Order Service is down, consumers cannot process the event.

That may be acceptable in some designs, but it should be intentional.

Events should often contain enough data for common consumers to do their work independently.

Keep payloads understandable

An event payload should be readable.

If someone opens a message from a dead-letter queue, they should be able to understand what it represents.

For example:

{
  "eventType": "BonusGranted",
  "eventVersion": 1,
  "occurredAt": "2026-06-23T10:00:00Z",
  "payload": {
    "bonusGrantId": "bonus_123",
    "playerId": "player_456",
    "amount": 10,
    "currency": "EUR",
    "reason": "WelcomeBonus"
  }
}

That is understandable.

Compare that with:

{
  "t": "BG",
  "v": 1,
  "p": {
    "b": "bonus_123",
    "u": "player_456",
    "a": 10,
    "r": 3
  }
}

Compact formats may save space, but they make debugging harder.

There are cases where compact binary formats are useful, but the schema should still be well documented and inspectable through tooling.

Validate events before publishing

A producer should validate events before publishing them.

For example:

Required fields are present
Field types are correct
Enum values are valid
IDs are valid
Amounts include currency
Version is set
Metadata is present

Invalid events should not enter the broker if they can be caught at the producer.

A producer that publishes invalid events pushes complexity downstream.

Consumers should still protect themselves, but producers should be strict about what they emit.

Consumers should be tolerant, but not careless

Consumers should ignore unknown optional fields.

They should handle supported versions.

They should avoid breaking when new fields are added.

But that does not mean consumers should accept everything.

A consumer should reject or isolate events that are unsafe to process.

For example:

Missing required business ID
Unsupported event version
Invalid amount
Unknown critical enum value
Invalid state transition

The consumer should fail clearly, with enough context for debugging.

Tolerant does not mean silent.

Document the event

A good event should have documentation.

At minimum:

Event name
Business meaning
Producer
When it is published
Payload example
Required fields
Optional fields
Version
Compatibility rules
Known consumers
Retention and replay expectations

This documentation does not need to be heavy.

But the meaning of the event should not live only in one developer's head.

Events are shared contracts.

Shared contracts need shared understanding.

A practical event design checklist

When designing an event, I like to ask:

What business fact does this event represent?
Who owns this fact?
Is the event named in the past tense?
Which consumers are likely to use it?
Does it contain the IDs needed for idempotency?
Does it contain the version needed for ordering?
Does it include correlation and causation IDs?
Can consumers process it without calling back?
Does it avoid leaking internal database details?
Does it avoid unnecessary sensitive data?
Are field names clear and unambiguous?
Can the schema evolve safely?
Is the event documented?

These questions catch many design problems early.

The interview version

If I had to explain event payload design in an interview, I would say:

An event should represent a clear business fact, usually named in the past tense, like OrderCreated or PaymentAuthorized. I would avoid exposing internal database changes as public events because events are contracts between services.

I like using an event envelope with metadata such as eventId, eventType, eventVersion, occurredAt, producer, correlationId, and causationId. The payload should contain the business identifiers and data needed by consumers.

The amount of data depends on the use case. Thin events with only IDs are simple, but they may force consumers to call back to the producer, creating runtime coupling. Rich events can help consumers work independently, but the schema needs careful versioning and should not expose unnecessary internal or sensitive data.

I would include stable business IDs for idempotency, version or sequence numbers when ordering matters, clear units and currencies for numeric fields, and reason fields for failures or cancellations. The goal is that consumers can process the event safely, understand its meaning, and evolve with the schema over time.

Final thought

Event payloads are easy to underestimate.

At first, they look like just JSON messages.

But over time, they become contracts, audit records, replay inputs, debugging tools, and integration points between teams.

A good event payload makes the system easier to operate.

A bad event payload creates coupling, confusion, and fragile consumers.

Design events around business facts.

Include the metadata needed to trace them.

Include the identifiers needed to process them safely.

Include enough data for consumers to do their work, but not so much that the event becomes a database dump.

And most importantly, treat the event as something other systems will rely on.

Because they will.

This post is part of my Backend Architecture Notes series.