The starting point
When services talk only through events, two things break quietly: a change and its event committing separately, so the platform drifts; and a consumer doing the work twice when a message is redelivered. This service shows both solved properly.
What we were responsible for
We designed and built the service, its tests and the architecture records.
What we built
-
The lifecycle is a state machine
Allowed transitions live in one place in the model, and the API has no way to set a status directly.
-
Transactional outbox
Domain events are saved in the same transaction as the change and published to Kafka afterwards — proven by an integration test against a real database.
-
Idempotent consumers
Incoming events are applied once even when Kafka redelivers them, with backoff and a dead-letter topic.
-
Layering enforced by the build
Architecture tests fail the build if the domain imports a framework, a transaction appears outside the application layer, or code reads the clock directly.
-
Decisions written down
Decision records for the outbox, idempotent consumers and the framework-free domain.
Where it stands
The full suite, including integration tests on real MySQL and Kafka containers, runs on GitHub Actions on every push.