Events as Contracts
The only coupling between slices should be the events themselves.
The Principle
A slice declares:
- Input events — what it reads
- Output events — what it emits
No shared code. No shared schemas. No imports across slice boundaries.
Slice A Slice B
┌──────────────────┐ ┌──────────────────┐
│ Reads: │ │ Reads: │
│ UserCreated │ │ OrderPlaced │
│ │ │ UserCreated │
│ Emits: │ │ │
│ OrderPlaced │ │ Emits: │
│ │ │ InvoiceSent │
└──────────────────┘ └──────────────────┘
│ │
└───────────────────────────┘
│
Shared event log
(append-only)
The event log is the only integration point.
Each Slice Owns Its Types
Slice A defines its own UserCreated:
Slice B defines its own UserCreated:
No shared import. Each slice deserializes what it needs. Unknown fields are ignored.
Why This Works
1. No compile-time coupling
Slices don't import each other. Adding a field to an event doesn't break consumers.
2. Independent evolution
Slice A can add PhoneNumber to its local UserCreated definition without touching Slice B.
3. Deploy independently
No coordination. Slice B doesn't need to redeploy when Slice A changes.
4. True vertical slicing
Each slice is a standalone module. The event log is just a message bus with a contract: event type + shape.
The Contract
Events are the API between slices:
| Contract element | Who defines it |
|---|---|
| Event type name | Producer |
| Required fields | Producer documents, consumers pick what they need |
| Tags (entity scope) | Producer |
Consumers are responsible for handling schema evolution (missing fields → defaults, extra fields → ignored).
In Fairway
Each command defines its own local event types:
// createlist/command.go
type ListCreated struct {
ListId string
Name string
}
func (cmd CreateList) Run(ctx context.Context, ra fairway.EventReadAppender) error {
// reads only what this command needs
// emits ListCreated with its own type definition
}
No shared events/ package. No domain model. Just the event contract.
Tradeoffs
| Benefit | Cost |
|---|---|
| Zero compile-time coupling | Type names must match across slices |
| Independent deploys | Must document event shapes |
| Each slice picks its own fields | No compiler catching mismatches |
The benefit — true slice independence — usually outweighs the cost.
Next
Events solve the coupling problem. But how do you get consistency without shared streams?