# Event API Specification: [Event domain or application name]

> Complete specification for an event-driven, asynchronous interface:
> brokers, channels, operations, messages and schemas, envelope, ordering,
> delivery guarantees, retries and dead-letter handling, schema evolution,
> security and consumer onboarding. The structure follows the concepts of
> the AsyncAPI Specification 3.0 (servers, channels, operations, messages,
> components) and uses the CloudEvents 1.0 envelope. Replace every
> `[bracketed]` value and every row marked *Example*. Worked examples use
> an order-fulfilment event stream.

<!--
Guidance: How to use this template
- Decide the delivery contract (section 7) and the evolution rules (section 9)
  before you publish the first event. They are the hardest things to change
  once consumers exist.
- Treat every event as a public API. Name it as a past-tense business fact
  (OrderShipped), not as a command or a database change (UpdateOrderRow).
- Keep the AsyncAPI document (section 11) and the schema registry as the
  machine-readable truth; this document explains intent and rules.
- Delete sections that do not apply but leave one line saying why.
-->

## Document control

<!-- Guidance: Identify the specification, the producing application and the version of the event contract it describes. -->

| Field | Value |
|---|---|
| Document ID | [EVT-SPEC-FULFIL-001] |
| Producing application | [Fulfilment service] |
| Event contract version | [1.2.0] |
| Document version | [0.1] |
| Status | [Draft / In review / Approved / Deprecated] |
| Owner (team) | [@team-fulfilment] |
| Classification | [Internal / Partner / Public] |
| Last updated | YYYY-MM-DD |
| Related | [AsyncAPI file path], [Schema registry subject], [REST API spec] |

### Approvals

| Role | Name | Decision | Date |
|---|---|---|---|
| Producer owner | [@name] | [Approved / Changes requested] | YYYY-MM-DD |
| Platform / messaging team | [@name] | [Decision] | YYYY-MM-DD |
| Security | [@name] | [Decision] | YYYY-MM-DD |
| Consumer representative | [@name] | [Decision] | YYYY-MM-DD |

## Revision history

<!-- Guidance: Record contract changes: new events, new fields, deprecations. Mark breaking changes clearly and link the migration note. -->

| Version | Date | Author | Change summary |
|---|---|---|---|
| 0.1 | YYYY-MM-DD | [@name] | First draft |
| 0.2 | YYYY-MM-DD | [@name] | *Example:* Changed partition key from `customerId` to `orderId` |
| 1.0 | YYYY-MM-DD | [@name] | *Example:* Approved for production |

## Contents

<!-- Guidance: Keep this list aligned with the headings. -->

1. [Introduction](#1-introduction)
2. [Servers and brokers](#2-servers-and-brokers)
3. [Channels](#3-channels)
4. [Operations](#4-operations)
5. [Messages and schemas](#5-messages-and-schemas)
6. [Ordering and partitioning](#6-ordering-and-partitioning)
7. [Delivery semantics](#7-delivery-semantics)
8. [Retries and dead-letter handling](#8-retries-and-dead-letter-handling)
9. [Schema evolution and compatibility](#9-schema-evolution-and-compatibility)
10. [Security](#10-security)
11. [AsyncAPI document](#11-asyncapi-document)
12. [Interaction flows](#12-interaction-flows)
13. [Operations and service levels](#13-operations-and-service-levels)
14. [Consumer onboarding](#14-consumer-onboarding)
15. [Glossary and references](#15-glossary-and-references)

## 1. Introduction

<!-- Guidance: Explain what business facts this stream carries, who produces them and who is expected to consume them. Name the bounded context. -->

### 1.1 Purpose

[What the events communicate and why an asynchronous interface was chosen.
Example: "The Fulfilment service publishes the lifecycle of each
shipment so that billing, notifications and analytics can react without
calling Fulfilment synchronously."]

### 1.2 Scope and context

| Item | Value |
|---|---|
| Bounded context | *Example:* Fulfilment |
| Producer(s) | *Example:* `fulfilment-service` (sole writer of every channel in this spec) |
| Known consumers | *Example:* `billing-service`, `notification-service`, analytics lake ingest |
| Interaction style | *Example:* Event notification with full state (event-carried state transfer) |
| Out of scope | *Example:* Commands to Fulfilment (use the REST API), inventory events |

### 1.3 Event design principles

- Events describe facts that already happened, named in the past tense.
- Each event type has exactly one producing application.
- Payloads carry enough state for typical consumers to act without a
  call-back; sensitive data is excluded unless listed in section 10.
- Consumers must tolerate duplicates, gaps in optional fields and unknown
  fields.

## 2. Servers and brokers

<!-- Guidance: List every broker endpoint consumers connect to, the protocol and the environment. In AsyncAPI terms these are the servers. -->

| Server ID | Environment | Host | Protocol | Version | Notes |
|---|---|---|---|---|---|
| `prod` | Production | `[broker-1.prod.example.com:9093]` | Kafka | [3.7] | TLS and SASL/OAUTHBEARER |
| `staging` | Staging | `[broker-1.stg.example.com:9093]` | Kafka | [3.7] | Masked data |
| `sandbox` | Partner sandbox | `[events.sandbox.example.com]` | AMQP 0-9-1 / MQTT / WebSocket | [Version] | [Bridge for external partners] |
| [ID] | [Environment] | [Host] | [Protocol] | [Version] | [Notes] |

## 3. Channels

<!-- Guidance: A channel is the addressable place messages travel through: a topic, queue or routing key. Document the address, which messages it carries and its configuration. -->

| Channel ID | Address | Messages | Partitions | Retention | Cleanup |
|---|---|---|---|---|---|
| CH-01 | `fulfilment.shipment.v1` | ShipmentCreated, ShipmentDispatched, ShipmentDelivered, ShipmentFailed | 24 | 7 days | delete |
| CH-02 | `fulfilment.shipment-state.v1` | ShipmentState (latest snapshot per shipment) | 24 | infinite | compact |
| CH-03 | `fulfilment.shipment.v1.dlq` | Any message from CH-01 that failed consumer processing | 6 | 30 days | delete |
| [CH-nn] | [Address] | [Messages] | [n] | [Retention] | [Policy] |

Channel naming convention: `<domain>.<entity>[-<qualifier>].v<major>`,
lower case, dot-separated. A new major version means a new channel.

## 4. Operations

<!-- Guidance: In AsyncAPI 3 an operation states what the application does on a channel: send or receive. Document both what this application sends and anything it receives. -->

| Operation ID | Action | Channel | Message(s) | Trigger | Expected rate |
|---|---|---|---|---|---|
| OP-01 | send | CH-01 | ShipmentCreated | Label purchased from carrier | *Example:* 40/s average, 300/s peak |
| OP-02 | send | CH-01 | ShipmentDispatched | Carrier scan at depot | 35/s average |
| OP-03 | send | CH-01 | ShipmentDelivered | Carrier delivery confirmation | 30/s average |
| OP-04 | send | CH-02 | ShipmentState | Every change on CH-01 | Same as CH-01 total |
| OP-05 | receive | `orders.order.v1` | OrderPaid | Starts picking for a paid order | 45/s average |
| [OP-nn] | [send / receive] | [Channel] | [Message] | [Trigger] | [Rate] |

## 5. Messages and schemas

<!-- Guidance: Every message has an envelope (headers and metadata) and a payload with a registered schema. Show a complete example for each message type. -->

### 5.1 Envelope: CloudEvents attributes

All messages use the CloudEvents 1.0 envelope in binary content mode:
attributes travel as protocol headers (prefixed `ce_` on Kafka) and the
payload is the message body. Examples in this document use the
structured JSON form for readability. `partitionkey` comes from the
CloudEvents partitioning extension.

| Attribute | Required | Value rule | Example |
|---|---|---|---|
| `specversion` | Yes | Always `1.0` | `1.0` |
| `id` | Yes | Unique per event from this source; UUID or ULID | `01HV7A0B2C9D8E7F6G5H4J3K2L` |
| `source` | Yes | URI reference of the producing application | `/fulfilment-service/eu-west-1` |
| `type` | Yes | Reverse-DNS event type with major version | `com.example.fulfilment.shipment.dispatched.v1` |
| `time` | Yes | RFC 3339 UTC time the fact occurred | `2026-03-15T14:02:11Z` |
| `subject` | Yes | ID of the entity the event is about | `shp_01HV79ZZ0K` |
| `datacontenttype` | Yes | Payload media type | `application/avro` / `application/json` |
| `dataschema` | Yes | URI of the schema version in the registry | `https://schemas.example.com/fulfilment/shipment-dispatched/3` |
| `partitionkey` | Yes | Ordering key (section 6) | `ord_01HV6Z3K8Q` |
| `traceparent` | Yes | W3C Trace Context for distributed tracing | `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01` |
| [extension] | [Yes / No] | [Rule] | [Example] |

### 5.2 Message catalogue

| Message | Type attribute | Schema subject | Format | Key fields |
|---|---|---|---|---|
| ShipmentCreated | `com.example.fulfilment.shipment.created.v1` | `fulfilment.shipment-created` | Avro | shipmentId, orderId, carrier, serviceLevel |
| ShipmentDispatched | `com.example.fulfilment.shipment.dispatched.v1` | `fulfilment.shipment-dispatched` | Avro | shipmentId, orderId, trackingNumber, dispatchedAt |
| ShipmentDelivered | `com.example.fulfilment.shipment.delivered.v1` | `fulfilment.shipment-delivered` | Avro | shipmentId, orderId, deliveredAt, proofOfDelivery |
| ShipmentFailed | `com.example.fulfilment.shipment.failed.v1` | `fulfilment.shipment-failed` | Avro | shipmentId, orderId, reasonCode |
| [Message] | [Type] | [Subject] | [Format] | [Fields] |

### 5.3 Example: ShipmentDispatched

Payload fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `shipmentId` | string | Yes | Shipment ID, prefix `shp_` |
| `orderId` | string | Yes | Order the shipment belongs to |
| `carrier` | enum | Yes | `DHL`, `UPS`, `LA_POSTE`, `OTHER`; consumers must accept unknown values |
| `trackingNumber` | string | Yes | Carrier tracking number |
| `dispatchedAt` | timestamp | Yes | Time of the first carrier scan |
| `parcelCount` | int | Yes | Number of parcels, at least 1 |
| `estimatedDeliveryDate` | date | No | Carrier estimate, if supplied |

```json
{
  "specversion": "1.0",
  "id": "01HV7A0B2C9D8E7F6G5H4J3K2L",
  "source": "/fulfilment-service/eu-west-1",
  "type": "com.example.fulfilment.shipment.dispatched.v1",
  "time": "2026-03-15T14:02:11Z",
  "subject": "shp_01HV79ZZ0K",
  "datacontenttype": "application/json",
  "dataschema": "https://schemas.example.com/fulfilment/shipment-dispatched/3",
  "partitionkey": "ord_01HV6Z3K8Q",
  "data": {
    "shipmentId": "shp_01HV79ZZ0K",
    "orderId": "ord_01HV6Z3K8Q",
    "carrier": "LA_POSTE",
    "trackingNumber": "6A12345678901",
    "dispatchedAt": "2026-03-15T14:02:11Z",
    "parcelCount": 1,
    "estimatedDeliveryDate": "2026-03-17"
  }
}
```

### 5.4 Example: [Message name]

[Repeat the field table and a complete example for every message in 5.2.]

## 6. Ordering and partitioning

<!-- Guidance: Brokers only guarantee order within a partition. State the ordering key, what order consumers can rely on and what they cannot. -->

| Topic | Rule |
|---|---|
| Partition key | *Example:* `orderId`, so all shipment events for one order land in one partition |
| Order guaranteed | Events for the same `orderId` are delivered in the order they were produced |
| Order not guaranteed | Across orders; across channels (CH-01 versus `orders.order.v1`) |
| Partition count | 24; changes only with a migration plan because key-to-partition mapping changes |
| Hot keys | One order rarely exceeds 10 events; no hot-key mitigation needed |
| Out-of-order handling | Consumers compare `time` and a per-entity `sequence` field and drop older updates |

The partition for a key is chosen by hashing:

$$
p = \text{hash}(\text{partitionkey}) \bmod P
$$

so increasing the partition count $P$ remaps keys and can break ordering
during the change.

## 7. Delivery semantics

<!-- Guidance: Be precise. Most systems deliver at least once; then every consumer must be idempotent. Say how the producer avoids losing events (for example the transactional outbox). -->

### 7.1 Producer guarantees

- Delivery: **at least once**. Duplicates are possible after producer
  retries or failover.
- No silent loss: events are written to a transactional outbox table in
  the same database transaction as the state change, and a relay publishes
  them with broker acknowledgement from all in-sync replicas (`acks=all`).
- Producer idempotence is enabled to reduce, not eliminate, duplicates.
- Maximum publish delay from state change to broker: [5 s at p99].

### 7.2 Consumer obligations

| Obligation | How |
|---|---|
| Be idempotent | Store processed `id` values (or the entity version) and skip repeats for at least the retention period |
| Commit after processing | Commit offsets only after side effects are durable |
| Tolerate unknown fields and enum values | Use the reader schema; map unknown enums to a default branch |
| Handle out-of-order events | Apply only if the event is newer than the stored state |
| Bound processing time | Keep each message under [30 s] or move work to a job queue |

### 7.3 Exactly-once statements

[State whether any end-to-end exactly-once processing is offered. Example:
"Not offered. Consumers achieve effectively-once results through
idempotent processing as described in 7.2."]

## 8. Retries and dead-letter handling

<!-- Guidance: Define who retries, how many times, with what delay, and what happens to a poison message. A blocked partition is an outage. -->

| Failure type | Example | Consumer behaviour |
|---|---|---|
| Transient | Downstream timeout, database deadlock | Retry in place up to 3 times with backoff 1 s, 5 s, 25 s |
| Persistent but recoverable | Referenced customer not yet replicated | Publish to retry channel with delay 5 min, maximum 6 attempts |
| Poison | Schema-valid but semantically invalid payload | Publish to CH-03 dead-letter channel immediately |
| Deserialisation error | Unknown schema ID | Publish raw bytes to CH-03 with error header |

Dead-letter messages carry the original headers plus `dlq_reason`,
`dlq_consumer`, `dlq_attempts` and `dlq_first_failure_time`.

| DLQ process step | Rule |
|---|---|
| Alert | Any message in CH-03 pages the owning consumer team during business hours; more than [100] in 10 min pages on call |
| Triage | Within [1 business day] |
| Replay | Fix, then replay with the platform replay tool, preserving `id` so idempotency still applies |
| Discard | Only with a ticket recording the reason and approver |

## 9. Schema evolution and compatibility

<!-- Guidance: State the compatibility mode enforced by the registry and give concrete examples of allowed and forbidden changes. -->

### 9.1 Compatibility mode

The schema registry enforces **[BACKWARD_TRANSITIVE / FULL_TRANSITIVE]**
compatibility for every subject in this specification: a consumer using
any newer schema can read all data written with older schemas.

| Change | Allowed in the same major version? |
|---|---|
| Add an optional field with a default | Yes |
| Remove an optional field that has a default | Yes, after deprecation notice |
| Add a value to an enum | Yes, consumers must handle unknown values |
| Add a required field without a default | No |
| Rename a field or change its type | No |
| Change the meaning or unit of a field | No |
| Change the partition key | No; new channel version |

### 9.2 Breaking changes

A breaking change creates a new major event type (for example `com.example.fulfilment.shipment.dispatched.v2`) on a new
channel. The producer dual-publishes `v1` and `v2` for at least
[6 months]; `v1` is then sunset after every registered consumer confirms
migration.

### 9.3 Deprecation of fields

Mark deprecated fields with `"deprecated": true` in the schema doc string,
announce in the changelog, keep populating them for [3 months], then
remove only if the compatibility mode allows it.

## 10. Security

<!-- Guidance: Cover authentication to the broker, authorisation per channel, encryption, and what data may appear in events. -->

| Control | Rule |
|---|---|
| Transport | TLS 1.2 or later between clients and brokers and between brokers |
| Authentication | *Example:* SASL/OAUTHBEARER with workload identity; no shared passwords |
| Authorisation | ACLs per principal: producer has WRITE on CH-01 and CH-02 only; each consumer group has READ on named channels |
| Personal data | *Example:* No names, addresses or phone numbers in payloads; consumers fetch them from the Customer API when authorised |
| Encryption at rest | Broker disks encrypted with provider-managed keys; [field-level encryption for `proofOfDelivery` image URL] |
| Integrity | Schemas validated at produce time; unknown producers rejected by ACL |
| Audit | Broker audit log of ACL changes and authentication failures retained [1 year] |

## 11. AsyncAPI document

<!-- Guidance: Keep the full AsyncAPI document in the repository next to the producer code, lint it in CI and generate documentation from it. -->

The authoritative machine-readable contract is `[asyncapi/fulfilment.yaml]`.
The skeleton shows the AsyncAPI 3.0 structure used here.

```yaml
asyncapi: 3.0.0
info:
  title: Fulfilment Events
  version: 1.2.0
  description: Shipment lifecycle events published by fulfilment-service.
defaultContentType: application/json
servers:
  prod:
    host: broker-1.prod.example.com:9093
    protocol: kafka
    security:
      - $ref: '#/components/securitySchemes/oauth'
channels:
  shipment:
    address: fulfilment.shipment.v1
    messages:
      shipmentDispatched:
        $ref: '#/components/messages/ShipmentDispatched'
operations:
  publishShipmentDispatched:
    action: send
    channel:
      $ref: '#/channels/shipment'
    messages:
      - $ref: '#/channels/shipment/messages/shipmentDispatched'
components:
  securitySchemes:
    oauth:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://auth.example.com/oauth2/token
          availableScopes:
            events:fulfilment:read: Read fulfilment events
  messages:
    ShipmentDispatched:
      name: ShipmentDispatched
      contentType: application/json
      headers:
        type: object
        properties:
          ce_id: {type: string}
          ce_type: {type: string, const: com.example.fulfilment.shipment.dispatched.v1}
          ce_time: {type: string, format: date-time}
      payload:
        $ref: '#/components/schemas/ShipmentDispatchedPayload'
      bindings:
        kafka:
          key:
            type: string
            description: orderId
  schemas:
    ShipmentDispatchedPayload:
      type: object
      required: [shipmentId, orderId, carrier, trackingNumber, dispatchedAt, parcelCount]
      properties:
        shipmentId: {type: string}
        orderId: {type: string}
        carrier: {type: string}
        trackingNumber: {type: string}
        dispatchedAt: {type: string, format: date-time}
        parcelCount: {type: integer, minimum: 1}
        estimatedDeliveryDate: {type: string, format: date}
```

## 12. Interaction flows

<!-- Guidance: Show the end-to-end flow for the main scenarios, including the outbox, broker, consumers and the failure path. -->

The main flow from a carrier scan to downstream reactions, including the
outbox relay and a consumer failure routed to the dead-letter channel:

```mermaid
sequenceDiagram
  participant Carrier
  participant F as Fulfilment service
  participant DB as Fulfilment DB + outbox
  participant R as Outbox relay
  participant B as Broker
  participant Bill as Billing consumer
  participant N as Notification consumer
  Carrier->>F: Dispatch scan webhook
  F->>DB: Update shipment and insert outbox row (one transaction)
  R->>DB: Poll unpublished rows
  R->>B: Publish ShipmentDispatched (key orderId)
  B-->>R: Ack (all in-sync replicas)
  R->>DB: Mark row published
  B->>Bill: Deliver
  Bill->>Bill: Check processed id, create invoice
  B->>N: Deliver
  N->>N: Send email fails 3 times
  N->>B: Publish to DLQ
```

| Scenario | Producer events | Expected consumer reaction |
|---|---|---|
| *Example:* Normal delivery | Created, Dispatched, Delivered | Billing invoices on Dispatched; Notification emails on each |
| *Example:* Failed delivery | Created, Dispatched, Failed | Notification emails customer; Support case opened |
| [Scenario] | [Events] | [Reaction] |

## 13. Operations and service levels

<!-- Guidance: State what consumers can rely on and what the producer monitors. -->

| Indicator | Objective | Measured as |
|---|---|---|
| Publish latency | p99 at or below 5 s from state change to broker | Outbox row timestamp to broker timestamp |
| Producer availability | 99.9% of events published within 60 s, monthly | Outbox backlog age |
| Broker availability | [99.95%] monthly | Platform SLO |
| Consumer lag (per consumer, owned by consumer) | Below [60 s] p95 | Lag exporter |
| [Indicator] | [Objective] | [Method] |

Replay: consumers may reset offsets within the retention window. Requests
to re-publish older history go to [@team-fulfilment] with [5 business
days] notice.

## 14. Consumer onboarding

<!-- Guidance: A checklist a new consuming team follows. Every step should have an owner and an observable outcome. -->

| Step | Action | Owner | Done when |
|---|---|---|---|
| 1 | Read this specification and the AsyncAPI file | Consumer team | Questions raised in [channel] |
| 2 | Register as a consumer: name, contact, channels, purpose, data needs | Consumer team | Entry in consumer registry |
| 3 | Request ACLs and a consumer group ID (`<team>.<app>.<purpose>`) | Platform team | ACL applied in staging |
| 4 | Implement idempotency, DLQ and unknown-field handling per section 7 and 8 | Consumer team | Code review checklist passed |
| 5 | Run contract tests against registry schemas | Consumer team | CI green |
| 6 | Load test at [2x] expected peak in staging | Consumer team | Lag stays below objective |
| 7 | Add lag and DLQ alerts to the team dashboard | Consumer team | Alerts fire in a test |
| 8 | Production ACL approval | Producer owner and Security | ACL applied in production |

Registered consumers:

| Consumer | Team | Channels | Contact | Since |
|---|---|---|---|---|
| *Example:* `billing-service` | Billing | CH-01 | [@billing-oncall] | YYYY-MM-DD |
| [Consumer] | [Team] | [Channels] | [@contact] | YYYY-MM-DD |

## 15. Glossary and references

<!-- Guidance: Define messaging terms so producers and consumers mean the same thing. -->

### 15.1 Glossary

| Term | Meaning |
|---|---|
| Channel | Addressable destination for messages, such as a topic or queue |
| Operation | What an application does on a channel: send or receive |
| Envelope | Metadata wrapped around a payload (here CloudEvents attributes) |
| Partition key | Value that decides which partition receives a message and therefore its ordering scope |
| Outbox | Table written in the same transaction as business state, later relayed to the broker |
| Dead-letter queue (DLQ) | Channel holding messages that could not be processed |
| Compatibility mode | Registry rule deciding which schema changes are accepted |
| [Term] | [Meaning] |

### 15.2 References

| Reference | Used for |
|---|---|
| AsyncAPI Specification 3.0 | Structure of sections 2 to 5 and 11 |
| CloudEvents Specification 1.0 (CNCF) | Envelope attributes in 5.1 |
| W3C Trace Context | `traceparent` propagation |
| RFC 3339 | Timestamps |
| [Schema registry documentation] | Compatibility modes |
| [Internal messaging standard] | [Use] |

## Standard and licence note

Structure aligned with the AsyncAPI Specification 3.0 (Apache License
2.0): servers, channels, operations, messages and reusable components,
with the CloudEvents 1.0 envelope (CNCF, Apache License 2.0). Based on
common industry practice for event-driven integration, including the
transactional outbox and dead-letter channels. This template is original
text written for this collection and reproduces no text from those
specifications; read them for their normative requirements. Template
licence: MIT.
