Event versioning is harder than API versioning

An API version can be retired once the clients have moved. An event may be sitting in a queue, or in an event store, published by a version of the system that no longer exists — so old shapes have to be readable indefinitely.

{
  "type": "order.shipped",
  "version": 2,
  "data": { "order_id": 8841, "carrier_code": "UPS" }
}

// v1 had 'carrier' as a display name; the consumer upcasts on read
if ($envelope['version'] === 1) {
    $envelope['data']['carrier_code'] = Carrier::codeFor($envelope['data']['carrier']);
}

Additive changes need no version bump if consumers ignore unknown fields, which is worth establishing as a rule on day one. For anything else, upcasting on read — translating old shapes to the current one at the boundary — keeps the domain code on a single version. The alternative, handling every version everywhere, does not survive the third one.