Context
In an event-driven microservices estate, services need to publish domain events when their state changes, and those events have to be reliable: never lost, never published for a change that was later rolled back. The standard answer is the transactional outbox pattern, which isn't hard to get right once. The catch was that every team was solving it on its own, some correctly and some subtly wrong.
This was my graduation project: design and build a reusable framework that turns reliable event publishing into a solved, drop-in concern instead of bespoke plumbing per service.
The problem
The obvious approach, write to the database and then publish to Kafka, has a dual-write failure mode: the transaction commits but the publish fails (or vice versa), and now the event stream disagrees with the source of truth. Each team solved this independently, which meant:
- Repeated effort. Outbox tables, polling/relay logic, serialization, retry and ordering concerns, re-implemented service by service.
- Inconsistent correctness. Subtle differences in how each team handled failure meant the reliability guarantee wasn't actually uniform.
- Cognitive load in the wrong place. Teams were spending design energy on infrastructure plumbing instead of their actual domain.
Options considered
Application-level outbox relay
Each service writes to an outbox table in the same transaction as its business change, and a relay process reads the table and publishes to Kafka. Correct, but every service still owns the relay, the polling, and the failure handling, so the duplication problem stays.
Publish directly, accept eventual reconciliation
Skip the outbox and reconcile drift after the fact. Rejected: reconciliation is harder to reason about than prevention, and it weakens the very guarantee the framework exists to provide.
Change-Data-Capture via Debezium, abstracted into a framework
Let the database's commit log be the source of events: Debezium captures committed changes from the outbox and streams them to Kafka, so a change is published if and only if it committed. Wrap the outbox contract, serialization, and topic conventions in a framework teams adopt rather than rebuild.
The decision, and why
I built the framework around CDC with Debezium over a transactional outbox, packaged for Kotlin / Spring Boot services and designed with DDD boundaries so events map cleanly to domain concepts. The reasons:
- No dual-write hole. Tying publication to the commit log removes the failure mode entirely: there's no window where the DB and the stream can disagree.
- Adopt, don't rebuild. Teams get reliable publishing as a dependency and a small contract, not a plumbing project.
- Consistency across the estate. One implementation of the hard part means one place to reason about and improve the reliability guarantee.
This one is cleanly mine: sole designer and author, end to end, as my graduation project. The thesis was graded 8.5, and the framework's reuse across the estate was estimated to save on the order of 16 developer-years of duplicated outbox work. That's a projection, but a grounded one given the number of services that would otherwise build this themselves.
Outcome
Reliable event publishing became a drop-in concern. Instead of each team designing, building, and debugging its own outbox-and-relay, they depend on a single framework that closes the dual-write hole for them. The headline estimate of ~16 developer-years saved comes from the duplicated effort it removes across the microservices estate.
Reflection
The shift that interested me was one of framing. The valuable output wasn't the code that publishes one event; it was removing a decision from every future service. Designing for adoption (a small contract, sensible defaults, DDD-aligned boundaries) mattered more than any single technical trick. This is the project that pointed me most clearly toward platform and solution-architecture work.