Mesrai
Back to blog
// essayTechnical Deep Dive

Event Payload Schema Change Without Versioning: The Consumer Break

Real PR catch: Kafka event field rename — 3 consumers still on old name. Dual-publish + version fix.

Mesrai TeamAugust 18, 20268 min read

A Kafka publisher renaming `email` to `emailAddress` in the event payload. Three downstream consumers still read `email`. Mesrai flagged the cross-service contract break.

The vulnerable diff

tsevents/user-created.ts
// events/user-created.ts
- publish("user.created", { id: user.id, email: user.email });
+ publish("user.created", { id: user.id, emailAddress: user.email });

// downstream services still reading event.email:
//   - services/welcome-emailer: reads event.email → undefined
//   - services/crm-sync:        reads event.email → undefined
//   - services/marketing-bus:   reads event.email → undefined

What is wrong

Events are a distributed-system API contract. Renaming a field is a breaking change for every consumer. Unlike a synchronous HTTP API, you can't roll back a producer change after consumers have processed old events. The standard mitigations are: add a `v` (version) field to every event, keep both old and new field names during a transition window, and remove the old name only after telemetry confirms no consumer reads it. The discipline maps to OpenAPI Deprecation but for async events.

The attack

Production timeline without versioning:

textexploit
t+0 — Producer deploys with renamed field.
t+0 — welcome-emailer reads event.email = undefined → fails to send.
t+5min — crm-sync nulls out customer emails.
t+15min — Incident. Rollback producer.
t+1h — Producer rolled back. crm-sync data still corrupted.
t+1d — Hand-rolled migration to repair crm-sync records.

Async contracts demand more discipline than sync — there is no synchronous error to alert on, and rollback doesn't undo consumer side effects.

Mesrai's review comment

textreview-comment.txt
mesraipilot · Bot · reviewed 2 min ago

[mesrai] [code-review] [Logic] [Schema-Evolution] [critical]

Event payload field rename without versioning. Three known consumers
still read `event.email`:

  - services/welcome-emailer/handler.ts:18
  - services/crm-sync/sync.ts:42
  - services/marketing-bus/dispatch.ts:67

Phase the change:

  1. (This PR) Producer publishes BOTH names, bumps version:
     publish("user.created", {
       v: 2,
       id: user.id,
       email: user.email,         // legacy, keep for now
       emailAddress: user.email,  // new
     });

  2. Migrate each consumer to read `emailAddress`. Separate PRs per
     service, behind feature flags if needed.

  3. After all consumers are migrated AND telemetry confirms zero
     reads of `email`, remove the legacy field in a follow-up PR.

For new schemas, prefer a schema-registry (Confluent Schema Registry,
Apicurio) that enforces compatibility rules on every produce.

The fix

tsevents/user-created.ts (fixed)
// events/user-created.ts — fixed (transition phase)
publish("user.created", {
  v: 2,
  id: user.id,
  email: user.email,          // legacy name — keep until consumers migrate
  emailAddress: user.email,   // new canonical name
});

Both names in the payload during transition. `v: 2` lets consumers detect the new shape. Follow-up PR removes legacy after telemetry confirms no consumer reads it.

Why human review missed it

Event-schema discipline is missing from most teams without a schema registry. The bug surfaces only after producer deploy when consumers hit the old field. Mesrai cross-references publish calls against consumer code in the monorepo and flags renames that would break known consumers.

Related rules + further reading

Mesrai rule pack: logic/event-payload-versioning — flags event payload renames without backward compatibility.

Confluent Schema Registry — compatibility modes (BACKWARD, FORWARD, FULL).

Common in microservice architectures without a registry.

Takeaway

Event renames need backward compat. Keep both names. Migrate consumers. Remove old name last. Mesrai catches the break.

// try

See it on your next PR.

Free for individuals. Install in two minutes. Mesrai reviews every commit.