The Event Contract Is the Only Boundary That Matters


Why the schema of what your system publishes is more architecturally significant than the framework it runs on, the database it uses, or the team that built it.


There is a thought experiment worth doing before designing any distributed system.

Imagine you need to replace one of your services completely. Different language. Different database. Different team. The service has been running in production for three years. Other services depend on it. Replacing it without breaking anything is the problem.

What do you actually need to preserve?

Not the code — you are replacing that. Not the database schema — the new service will have its own. Not the framework — that is exactly what you are changing. Not the deployment model — that is part of why you are replacing it.

The only thing you need to preserve is what the service says to the rest of the system. The events it publishes. Their schema. Their semantics. Their ordering guarantees.

If the new service publishes identical events with identical schemas, nothing else in the system knows a replacement happened. The boundary held. Every subscriber keeps working. The migration is invisible.

This is what it means to say the event contract is the only boundary that matters. Not the only thing that matters — the only thing that constitutes a boundary between components. Everything else is internal.


What a Contract Actually Is

The word contract is used loosely in software. API contracts, database contracts, interface contracts. All useful ideas. The event contract is a specific thing.

An event contract defines:

What is published. The event type — ClassPublished, OrderCreated, PaymentCleared. A stable name that carries meaning to every subscriber.

What it contains. The schema — every field, every type, every optional value. classId is a string. scheduledStart is an ISO 8601 timestamp. capacity is a non-negative integer. These are commitments, not suggestions.

What it means. The semantics — what did the publisher observe or decide that caused this event to be emitted? ClassPublished means a class has been created and is available for booking. Not that a class might be created. Not that a class creation was requested. That it happened.

What consumers can expect. The ordering guarantees — if ClassUpdated follows ClassPublished for the same classId, consumers will see them in that order. If ordering is not guaranteed, the contract says so explicitly.

What it does not contain. The implementation details of the publisher — its database, its framework, its internal model. These are explicitly outside the contract. A consumer that depends on them has broken the boundary even if no API has been called.


The Line Between Inside and Outside

Every system has an inside and an outside. The inside is everything the component controls — its code, its database, its internal events, its local state. The outside is what it shares with other components.

The event contract is the precise definition of that line.

On the inside of the contract: anything can change. The publisher can restructure its database, rewrite its logic, change its framework, rename its internal classes. None of this affects subscribers. They receive the same events with the same schemas. The boundary is intact.

On the outside of the contract: nothing can change without coordination. Adding a new required field to ClassPublished breaks every consumer that does not expect it. Changing the semantics of scheduledStart from UTC to local time breaks every consumer that assumes UTC. Removing capacity breaks every consumer that reads it.

This asymmetry is the point. The inside is cheap to change. The outside is expensive to change. Keeping as much as possible on the inside — and making the outside as small as possible — is the governing principle of good distributed system design.

An event contract that contains only what consumers genuinely need, and no more, is a small outside. A small outside means cheap change. Cheap change means the system can evolve.


Why the Event Contract Outlasts the Code

Consider what actually happens to a production system over time.

The code gets rewritten. Frameworks change. Languages change. Performance requirements demand different implementations. Technical debt accumulates until a rewrite is cheaper than continued maintenance. Code has a half-life measured in years.

The database gets migrated. PostgreSQL to Aurora. MongoDB to a relational model. A normalisation that made sense at ten thousand records becomes a problem at ten million. Database schemas evolve — sometimes gradually, sometimes through painful migrations.

The team changes. The people who wrote the original service leave. New people arrive with different approaches, different languages they prefer, different patterns they trust. A service written by a Java team gets rewritten in Go by the team that replaced them.

The event contract, if designed carefully, outlasts all of this. ClassPublished published in 2025 and ClassPublished published in 2030 should be the same event. The subscribers from 2025 should still work in 2030. The contract is the stable thing in a system where everything else changes.

This is not aspirational. It is achievable. But only if the contract is treated as a first-class artifact — designed deliberately, versioned explicitly, changed with coordination — rather than a side effect of whatever the publisher happens to emit.


The Practical Cost of Getting This Wrong

When event contracts are not treated as boundaries, the coupling that was supposed to not exist simply becomes implicit rather than explicit.

The most common failure: consumers that read implementation details rather than contract fields. A consumer that queries the publisher's database directly to get information the event did not include. A consumer that parses the event's correlationId field to extract a database row ID that happens to be embedded there. A consumer that relies on a field the publisher did not intend to be part of the contract because it happened to be useful.

Each of these creates an invisible dependency. When the publisher changes its database schema, the consumer breaks — but not through any API that either team knew about. The coupling was real but undocumented. The boundary was an illusion.

The second failure: event schemas that carry too much. A ClassPublished event that includes the entire class object — every field in the publisher's database — because it was easier to dump everything than to decide what consumers actually need. Now every consumer has access to implementation details. Some consumer will eventually rely on a field that was never intended to be contractual. The boundary has been dissolved by over-sharing.

The third failure: semantic drift. ClassPublished starts meaning "a class was created." Over time, the publisher starts also emitting it when a class is updated significantly — because it is the easiest way to trigger consumers to re-fetch. Now ClassPublished means something different to the publisher than it means to consumers who built against the original semantics. The contract's meaning has changed without the contract being versioned.


What a Well-Designed Contract Looks Like

Take ClassPublished from a fitness platform as a concrete example.

A poorly designed version includes everything:

ClassPublished {
  id, title, classTypeName, coachId, coachName, coachEmail,
  coachPhone, locationId, locationName, locationAddress,
  locationCity, locationState, locationZip, locationLat,
  locationLng, capacity, confirmedCount, waitlistCount,
  start, end, timezone, ruleId, recurrenceType,
  recurrenceInterval, recurrenceDays, bookingOpensAt,
  bookingClosesAt, lateCancelCutoff, noticeHours,
  minEnrollment, autoCancel, eligibleInstruments,
  createdAt, updatedAt, publishedBy, publishedAt,
  internalNotes, adminFlags, ...
}

Every field from the publisher's database. Consumers get everything and use whatever seems useful. The publisher cannot change anything without potentially breaking someone.

A well-designed version contains only what consumers need to react correctly:

ClassPublished {
  classId:              string      — stable identifier, forever
  tenantId:             string      — which gym published this
  className:            string      — display name
  classType:            string      — for filtering and eligibility rules
  coach: {
    coachId:            string      — stable identifier
    coachName:          string      — display name
    needsCoverage:      boolean     — operational signal
  }
  location: {
    locationId:         string      — stable identifier
    locationName:       string      — display name
  }
  capacity:             integer     — for availability projection
  scheduledStart:       timestamp   — UTC, ISO 8601
  scheduledEnd:         timestamp   — UTC, ISO 8601
  bookingOpensAt:       timestamp   — UTC, ISO 8601
  bookingClosesAt:      timestamp   — UTC, ISO 8601
  lateCancelCutoff:     timestamp   — UTC, ISO 8601
  eligibleInstruments:  string[]    — which membership types can book
}

Forty fields reduced to eighteen. The publisher's internal fields — ruleId, recurrenceType, internalNotes, adminFlags — are not in the contract. Coach's contact details are not in the contract. Location coordinates are not in the contract. These are internal details. Consumers do not need them to react correctly to ClassPublished.

The publisher can change anything not in the contract — restructure its database, change its internal model, move coach details to a separate store — without affecting any subscriber. The contract is small. The inside is large. Change is cheap.


The Substitutability Test

The clearest test of whether an event contract is doing its job: can you substitute the publisher without subscribers knowing?

A fitness platform might have classes published by a native scheduling capability, by a third-party scheduling system, or by an adapter that translates a legacy system like Mindbody or PushPress. Three completely different systems. Three different databases. Three different teams or vendors.

If the event contract is well-defined, all three publish identical ClassPublished events. Reservation subscribes to ClassPublished. It builds its schedule projection from those events. It does not know — and must not need to know — which system published them.

This substitutability is not an accident. It is the architectural property the event contract creates. The contract defines the interface. The publisher is an implementation of that interface. Any implementation that honours the contract is substitutable.

When substitutability holds, the system can evolve freely. The platform operator can certify a new scheduling adapter. A gym can switch from a native capability to a third-party system. A team can rewrite the scheduling service. As long as the event contract is honoured, nothing downstream breaks.

When substitutability does not hold — when subscribers depend on something the publisher does not explicitly contract — the system is coupled in ways that are invisible until something breaks.


The Versioning Question

Event contracts change. Business requirements change. New fields become necessary. Old fields become irrelevant. The question is not whether contracts change but how.

Two principles hold:

Additive changes are safe. Adding a new optional field to ClassPublished does not break existing subscribers — they ignore fields they do not recognise. New subscribers can use the new field. The contract version does not need to increment.

Breaking changes require coordination. Removing a field, changing a field's type, changing a field's semantics — these require every subscriber to be updated before the publisher can deploy. In practice this means running two contract versions simultaneously for a migration window. Subscribers upgrade to the new version. The old version is deprecated. When all subscribers have migrated, the old version is retired.

The discipline of treating additive changes as safe and breaking changes as requiring coordination is what keeps the event contract stable over time. A publisher that removes fields without coordination has broken the boundary as surely as if it had changed its database schema without telling anyone.


The Claim

The event contract is the only boundary that matters in a distributed system because it is the only thing that defines what is inside and what is outside a component. Code is inside. Databases are inside. Frameworks are inside. What a component publishes to the rest of the system — the events, their schemas, their semantics — is outside.

A component that honours its event contract can be changed, rewritten, replaced, or extended freely. A component that violates it — or a subscriber that depends on something not in the contract — has created coupling that will eventually surface as a breaking change nobody predicted.

Design the contract deliberately. Keep it small. Treat additive changes as safe and breaking changes as requiring coordination. Version it explicitly. The code will change. The database will change. The team will change. The contract, designed well, outlasts all of them.


This post is part of a series on building modern distributed systems. The examples come from a fitness platform called OpenGym built from first principles — full event contracts, capability specifications, and adapter implementations available as an open reference.

Subscribe to 8861 Lab

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe