Create your own
Lesson illustration

Event Contract: Business Facts and Metadata Definition

Hello. In the previous lesson, you redesigned a synchronous order flow so that Order service can accept an order without waiting for Payment and Inventory to be available at that instant. That removes direct runtime coupling, but it creates a new intentional dependency: downstream services must interpret OrderPlaced, PaymentAuthorized, and similar messages consistently.

That shared interpretation is an event contract. It is more than a JSON class or a Kafka record value. A useful contract states: what fact happened, which occurrence this message represents, which business entity it concerns, when it happened, who is authoritative for it, and how it relates to the wider workflow.

By the end of this lesson, you will be able to define a practical event contract containing business facts, event identity, business identity, time, source, and correlation metadata—using a CloudEvents-style envelope that can be carried over Kafka, RabbitMQ, or HTTP.


An event contract is a public interface, not an internal DTO

An event is a durable statement made by the service that owns a business fact:

“Order service accepted order O-4821 at this time, with these agreed order details.”

This differs from serializing an internal Java entity and publishing it. An entity often contains implementation details, mutable fields, database-specific representation, or sensitive data that consumers neither need nor should depend on.

A contract has three connected layers:

LayerMain questionExample
Business semanticsWhat fact is being declared, and who is allowed to declare it?OrderPlaced is published by the Order domain after the order is accepted.
Data schemaWhat fields exist, what do they mean, and which are required?orderId, line items, total amount, and currency.
Envelope metadataHow can consumers identify, trace, correlate, and safely process this occurrence?Event ID, source, occurrence time, correlation ID, causation ID.

The schema is therefore a form of contract coupling. This is healthy coupling when it is explicit, documented, governed, and stable. It is much safer than consumers inferring meaning from an undocumented JSON blob.

Event Design and Event Streams Best Practices | Events and Event Streaming

Watch “Event Design and Event Streams Best Practices” from Confluent, an IBM Company for a compact explanation of why event schemas and metadata must be designed together.

Start with schemas. Focus on the comparison with database schemas: producers and consumers need an agreed layout, and that agreement must survive change over time. Then watch metadata and headers. Notice the distinction between a record’s business value, broker metadata such as partition and offset, and application-defined metadata such as origin and tracking information.

A broker can deliver bytes successfully even when the data is semantically wrong. For example, this event is syntactically valid JSON but a poor contract:

{
  "message": "order placed",
  "data": {
    "id": 4821
  }
}

A consumer cannot reliably tell:

  • whether 4821 identifies the event, the order, or the customer;
  • which service is asserting the fact;
  • when the fact occurred;
  • whether this is a duplicate;
  • whether it belongs to the checkout workflow it is currently processing;
  • what order placed means relative to OrderCreated, OrderConfirmed, or OrderSubmitted.

A production event must answer these questions without forcing every consumer to query the producing service.


Separate the business fact from its envelope

CloudEvents is a useful standard model for this separation. It defines a common event envelope around the application-specific data payload. You do not have to adopt every CloudEvents SDK immediately, but its vocabulary is a strong baseline for an enterprise-wide contract convention.

spec/primer.md at v1.0.1 · cloudevents/spec · GitHub

Read the CloudEvents Primer from the CNCF CloudEvents project to ground the difference between a fact-oriented event and a destination-oriented message, then connect event identity to its producer source.

In “CloudEvents Concepts,” read the distinction between events and intent-carrying messages. Then find “CloudEvent Attributes,” subsection “id,” and read the identity guidance. Pay particular attention to why an event ID exists for uniqueness, rather than as a substitute for every business identifier or workflow identifier.

Here is a realistic OrderPlaced contract in JSON CloudEvents structured form:

{
  "specversion": "1.0",
  "id": "01JNYQ9KBNK1Z70R49E86HCZFR",
  "type": "com.acme.commerce.order-placed.v1",
  "source": "urn:acme:commerce:orders",
  "subject": "orders/O-4821",
  "time": "2025-03-08T10:14:26.381Z",
  "datacontenttype": "application/json",
  "dataschema": "https://schemas.acme.example/orders/order-placed/1.0.0",
  "correlationid": "01JNYQ6TVENEA5MVSY6T1RPF8G",
  "causationid": "01JNYQ76N4WTH3HJHZ3XSSMNTJ",
  "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "data": {
    "orderId": "O-4821",
    "customerId": "C-881",
    "salesChannel": "WEB",
    "currency": "USD",
    "totalAmount": 129.50,
    "lines": [
      {
        "productId": "P-104",
        "quantity": 2,
        "unitPrice": 49.75
      },
      {
        "productId": "P-203",
        "quantity": 1,
        "unitPrice": 30.00
      }
    ]
  }
}
A CloudEvent represented as JSON: the required envelope attributes identify the event type, source, and occurrence; optional and extension attributes add context; the `data` object carries the application’s business facts.

The top-level fields form the envelope. The data object carries the business fact. Keeping them distinct lets infrastructure and generic tooling inspect event identity, source, and tracing context without needing to understand order line items.

A Spring Boot application may eventually serialize this object as a Kafka record value, put it in a RabbitMQ message body, or map some envelope values to protocol headers. The logical contract must remain the same even when its transport representation changes.


Define each piece of the contract precisely

The most common interview weakness is saying, “We will add an ID and timestamp,” without defining their semantics. The following table makes the meanings explicit.

FieldRequired?Meaning and rule
specversionYesVersion of the CloudEvents envelope specification, not the business event schema.
typeYesStable, machine-readable name for the business fact. Use a past-tense fact such as order-placed, never an imperative such as place-order.
sourceYesStable identifier for the logical producer that is authoritative for the fact. It is not a Kafka topic, RabbitMQ queue, consumer, or hostname.
idYesUnique identity of this specific event occurrence within its source. It remains unchanged if the same event is retried or replayed.
subjectUsuallyThe resource or entity the event is primarily about, such as orders/O-4821. It gives generic tooling a useful concise target.
timeYes in this conventionThe time at which the producer recognizes the business fact as having occurred, represented in UTC using RFC 3339 format.
datacontenttypeYesEncoding of the data payload, such as application/json.
dataschemaStrongly recommendedA stable reference to the payload schema and its documentation.
dataYesThe business facts that consumers may need to react without calling the producer synchronously.
correlationidYes for workflowsIdentifies the larger business interaction or workflow to which this event belongs.
causationidRecommendedIdentifies the immediate message or event that caused this one.
traceparentRecommendedDistributed tracing context, propagated according to the tracing convention in use.

The exact field names for extensions such as correlationid and causationid are a team convention. What matters is that the convention is consistent and documented. CloudEvents extension attribute names are normally lowercase, which is why the example uses correlationid rather than correlationId.

1. type: name the fact, not the processing instruction

com.acme.commerce.order-placed.v1 says:

  • it belongs to Acme’s commerce domain;
  • it reports the order-placed fact;
  • it is version 1 of that event type.

Its name must not encode a particular destination:

  • Good: com.acme.commerce.order-placed.v1
  • Poor: payment-service-process-order
  • Poor: send-order-to-inventory

The previous lesson established why. OrderPlaced may be useful to a workflow coordinator, analytics, notification, fraud detection, and a customer-history projection. Order service should not need to know which of those consumers exist.

2. Event identity is not business identity

The example has two different kinds of identity:

IdentifierExampleAnswers
Event identityid = 01JNYQ9KBNK1Z70R49E86HCZFR“Which exact occurrence or delivery-worthy fact is this?”
Business identitydata.orderId = O-4821“Which order is this fact about?”
Subject referencesubject = orders/O-4821“What resource is the primary subject, in a concise generic form?”

One order has many events over its lifetime:

Business factEvent IDOrder ID
Order placedA unique new valueO-4821
Payment authorizedA different unique valueO-4821
Inventory reservedA different unique valueO-4821
Order confirmedA different unique valueO-4821

Do not use orderId as the event ID. It would cause every later event about the same order to appear to be a duplicate.

Similarly, do not use a Kafka offset as the business event ID. Offsets identify a broker record’s position in one partition; they can change meaning when data is copied, replayed, or republished elsewhere. The producer-owned event ID travels with the logical event across systems.

For at-least-once delivery, a retry of the same publication must keep the same id. A consumer can then record a processed identity such as:

and recognize redelivery. A later, genuinely new fact about the same order receives a new event ID. You will implement persistent deduplication later; for now, the important point is that the contract supplies the identifier that makes it possible.

3. source identifies the authority, not an implementation detail

In the example:

urn:acme:commerce:orders

means the Orders business boundary is making this assertion. It should remain stable if:

  • Order service is renamed;
  • its code moves from a monolith into a microservice;
  • a deployment moves from one environment or cluster to another;
  • the service is scaled from two pods to twenty pods.

Avoid sources such as:

order-service-pod-7f69694bdf-qbt8p

That string identifies an ephemeral runtime instance, not the domain authority. Instance, host, region, deployment version, or tenant may be useful operational metadata, but they should be separate extension fields when genuinely needed.

4. Define what time means before incidents force the question

A timestamp has no value unless its semantic meaning is agreed.

For OrderPlaced, define time as:

The UTC time at which Order service committed acceptance of the order in its authoritative state.

This is not necessarily:

  • when the customer first clicked Place order;
  • when the HTTP request reached a load balancer;
  • when the producer retried publication;
  • when Kafka appended the record;
  • when a RabbitMQ consumer received it.

Those moments can all differ. If the customer submitted an order at 10:14:20, Order service committed it at 10:14:26, and the broker accepted a delayed retry at 10:15:03, the event’s business occurrence time should remain 10:14:26.

Broker timestamps, Kafka offsets, and queue delivery timestamps are operational facts about transport. They can help diagnose latency, but they must not silently replace the business occurrence time in the event contract.


Correlation, causation, and tracing answer different questions

These identifiers are often mistakenly merged into one field. They have different lifetimes and purposes.

Suppose a customer’s checkout request begins workflow W-9007.

Message in the workflowEvent or command IDCorrelation IDCausation ID
PlaceOrder commandM-100W-9007None, or the inbound request ID
OrderPlaced eventE-101W-9007M-100
AuthorizePayment commandM-102W-9007E-101
PaymentAuthorized eventE-103W-9007M-102
ReserveInventory commandM-104W-9007E-103

A correlation ID stays constant across the wider conversation. It answers:

“Which checkout, support case, batch, or distributed workflow does this message belong to?”

A causation ID changes at each hop. It answers:

“Which immediate message caused this message to be emitted?”

An event ID identifies only the message itself. It answers:

“Have I already processed this exact event occurrence?”

Finally, traceparent connects telemetry spans. It is excellent for following a request through logs and traces, but it should not replace a durable business correlation ID. Tracing may be sampled, storage may expire, and one business workflow can outlive a short-lived request trace.

3.1.0 | AsyncAPI Initiative for event-driven APIs

Read this short section of the AsyncAPI Specification to see how an API contract can formally state where consumers find correlation metadata.

Find “Correlation ID Object.” Read the contract definition, then continue into “Runtime Expression.” Read the location mechanism and inspect the header and payload examples below it. AsyncAPI does not force you to choose one field name; it lets a contract declare the authoritative location consumers must use.

For the course example, set this rule:

Every message participating in a customer order workflow carries correlationid. Every emitted message that was caused by another message also carries causationid.

That rule makes a production incident answerable. Given a customer complaint about O-4821, support can search by order ID. Given a workflow failure, operations can search by correlation ID. Given one suspicious event, engineers can walk backward through causation IDs.


Choose business facts deliberately

The data payload should contain facts that the Order domain is willing to publish and support as part of the event’s meaning.

For OrderPlaced, the example includes:

  • order and customer identifiers;
  • the accepted sales channel;
  • the agreed currency and total;
  • product identifiers, quantities, and unit prices.

This is a deliberate snapshot of the order at placement time. It should not change merely because a product’s current catalog price changes tomorrow.

Equally important is what it omits:

  • payment-card details;
  • internal database primary keys;
  • password or authentication information;
  • mutable internal workflow flags;
  • unrelated customer profile fields.

A useful rule is:

Include facts a legitimate consumer needs to react correctly; exclude data merely because it happens to be available in the producer’s entity model.

Whether to include full line items, only an order ID, or a more complete order snapshot is an important design choice. The next lesson will distinguish event notification from event-carried state transfer, which gives you a structured way to make that choice.


Transport metadata is not automatically part of the event contract

Kafka and RabbitMQ supply metadata of their own. Keep the layers separate.

ConcernKafka exampleRabbitMQ exampleContract decision
Logical event typeTopic, headers, or payloadExchange, routing key, headers, or bodyDefine type in the logical envelope.
Delivery locationTopic, partition, offsetExchange, queue, delivery tagTreat as transport metadata, not event identity.
Ordering keyKafka record keyUsually queue and consumer behaviorDefine the business entity identifier that will later inform key selection.
Trace and correlationRecord headers or bodyMessage headers or bodyDefine field names and semantics once, then map them consistently.
Payload encodingSerializer configurationcontentType property or body conventionState datacontenttype and schema explicitly.

For a Java codebase, a practical approach is to define a shared envelope model and validate it at the boundary where messages are published and consumed. Do not let every Spring Boot service invent its own spelling for eventId, event_id, messageUuid, requestId, and origin.

The goal is not necessarily a giant shared library. It is a shared contract. Teams may use independent implementations as long as they conform to the same documented schema and metadata rules.


A Technical Lead contract review checklist

Before approving a new event, review it in this order:

  1. Fact and authority
    Is the name a completed business fact, and is the producer genuinely authoritative for declaring it?

  2. Business identity
    Can a consumer identify the business entity, such as order, payment, shipment, or customer, without guessing?

  3. Event identity
    Is each event occurrence uniquely identified independently of the business entity, and preserved across publication retries?

  4. Time semantics
    Does the contract define exactly what the timestamp represents and use UTC?

  5. Source semantics
    Does source identify a stable business authority rather than a pod, hostname, broker topic, or queue?

  6. Workflow linkage
    Are correlation and causation identifiers present where the business flow spans services?

  7. Payload discipline
    Does data contain necessary business facts, while excluding secrets and internal persistence details?

  8. Documentation
    Are required fields, formats, examples, ownership, schema location, and compatibility expectations written down in an API contract such as AsyncAPI?


An event contract makes asynchronous collaboration intelligible and operable. The data payload states the business fact; id and source identify the precise event occurrence; the business entity ID identifies what the fact concerns; time defines when the fact became true; and correlation, causation, and tracing metadata connect the event to its workflow and diagnostics.

Next, you will decide how much business data an event should carry by distinguishing event notification from event-carried state transfer.

Can't find a good explanation? Sign up and we'll make it for you

Sign up