Making Eventing Easier to Build On
At MATTR, I supported a new eventing system and built internal libraries and code-generation tooling using TypeScript, Protobuf, and Kafka. I also worked on Kafka costs, and with my team on a global control-plane management API and authentication plane.
An eventing system can run, process messages, and demo well while making the next change frightening. The producer and consumer live in different services, but their assumptions are still tangled together. You end up with a distributed monolith and worse tooling.
The separation comes from deciding what a message means, who owns its contract, and how another engineer builds on it. The bus carries whatever you give it.
Decide whether it is a fact or a command
An event describes something that happened. A command asks something to happen. The distinction determines where responsibility lives.
Take an ordinary example: an order has been placed. One consumer might send an email; another might update an inventory view. The producer doesn't need to know about either behavior.
A message asking a consumer to send a confirmation email has a different contract: someone needs to own completing that action. Commands are useful, too. Trouble starts when both kinds of message become interchangeable and the word "event" hides the responsibility.
I prefer facts for describing changes other services may care about. It leaves a new consumer free to decide what that fact means to its own part of the system.
An event is a contract
A raw JSON payload can be easy to publish and hard to maintain. Each consumer has to work out which fields it can trust, what they mean, and what happens when the producer changes them. An event deserves the same care as a public API: an explicit schema, an owner, and compatibility rules.
A timestamp needs a meaning. An identifier needs a scope. Changing a field's meaning can break a consumer even when its type stays exactly the same. Those decisions belong in the contract, alongside the structure.
Protobuf gives you an explicit message schema and a binary encoding. One detail matters particularly when a contract evolves: field numbers identify fields on the wire. They need to remain stable, and a deleted field's number should be reserved rather than reused. The Protobuf language guide explains why.
The schema gives a review something concrete to discuss. What does the message promise? Can an older consumer still read it? What does an absent field mean? Compatibility checking can catch structural mistakes; somebody still has to review the meaning.
Code generation is part of the interface
Our code-generation tools used ts-morph alongside string literal templating. ts-morph wraps the TypeScript compiler API to make working with the abstract syntax tree, or AST, easier. An AST represents source code as structured nodes: declarations, expressions, imports, and their relationships.
This was a mix of structured generation and text templates. ts-morph let us work with TypeScript as code; string literals also formed part of the generated output. The useful distinction is how much structure a particular piece of generation needs. A short template can be easy to read, while imports and declarations benefit from tools that understand the source tree.
The useful question for a generator is what the next engineer has to decide. A new event needs a meaning and a schema. Repeating integration mechanics by hand adds another set of decisions, often without adding anything to the product.
I'd rather put those repetitive decisions into tooling and spend review time on the message itself. Generated code still needs to be understandable. An engineer should be able to inspect it, follow an import, and diagnose a type error without reverse-engineering the generator.
That is why I see generation and libraries as one interface. The generator creates a starting point; the library determines what using it feels like. Both need ordinary, inspectable behavior.
Design for a log
Kafka stores events in named streams called topics. A topic is split into partitions, each an ordered sequence of records. Ordering applies within a partition; there isn't one global order across all partitions in a topic.
Reading a record doesn't remove it. Consumers track their positions using offsets, and retained records can be read again. That makes replay useful, but the application has to be designed for it. Apache's Kafka introduction explains topics, partitions, and retention.
For a consumer that changes external state, I want to know what happens if it processes the same event again. Idempotency means that repeating an operation has the same effect as doing it once. Replays and retries make that a practical design question, independent of what the transport promises.
Kafka infrastructure cost optimisation was another part of my role. Storage, replication, capacity, and retention all have an operational cost. I start with what the product needs to guarantee and how the workload uses the system. A cost reduction is useful when the service can still meet those requirements.
Platform work crosses boundaries
Eventing sat alongside the team's work on an external global control-plane management API and a global authentication plane using Auth0 and OAuth2. Our services ran on AWS, with PostgreSQL in RDS for primary storage.
A control plane is the management side of a platform: the API through which its configuration and operation are controlled. That work and the authentication work gave the role a scope beyond the eventing system itself.
I was also a code owner responsible for code quality. I think that responsibility is best expressed through review questions the team can reuse: Is the contract clear? Is the behavior understandable? Can another engineer extend this without needing its author in the room?
Name the fact, own the contract, plan for repeated processing, and make the expected integration easy to write. Those decisions deserve attention before the next consumer depends on them. Internal libraries and tooling make them available to the whole team.