A learning project in event sourcing + Domain-Driven Design, built as a simulation of the payments seam in residential property management (NSW).
Latchkey models the boundary between two bounded contexts:
- Property Management (PM) — the core domain: tenancies, lease terms, expected rent, and therefore arrears. This is the deep context.
- Accounts — a thin supporting edge that emits payment facts (money received, reversals).
The interesting problem is how a payment fact born in Accounts crosses into PM
and reconciles into arrears (arrears = expected − received) — translated at an
anti-corruption layer, never folded raw across the seam.
Thesis / deliverable: a tenancy timeline — a complete, hash-chained history of a tenancy (rent falling due, payments, notices, corrections) legible enough to serve as evidence in an NCAT (tribunal) arrears case. That timeline is the payoff of an append-only event log with correction-by-compensation (never mutation).
It's a simulation, not production: events are seeded, and a tenant-behaviour engine plus scheduled jobs advance simulated time so histories accrue realistically rather than all at once. Grounded in the NSW Residential Tenancies Act 2010 for realism — not legal advice.
The model is worked out in docs/ and is the spec to build against:
docs/domain-model.md— the deep model: events, theTenancyaggregate, invariants, accrual, arrears (the time-based 14-day trigger), the two ACLs, and value objects.docs/context-map.md— the subdomain map: what's core / supporting / generic, and where the seams (language flips) are.CONTEXT.md— the ubiquitous language: terms only (ledger, arrears, property_ref, tenancy), the words the code and docs agree to use.docs/adr/— the decision log: why the model is shaped the way it is (e.g. ADR 0003 chose raw Commanded over Ash-native events). Read these before proposing a redesign.
The write side is live. Event sourcing runs on raw Commanded + its Postgres EventStore (chosen over Ash-native events in ADR 0003), with Ash for the read model:
- Property Management (
lib/latchkey/property_management/) — theTenancyaggregate with its commands and events, the arrears projector/fold (the 14-day gate), and the tenancy-timeline read model. - Accounts (
lib/latchkey/accounts/) — the thin edge emitting payment facts, translated into PM through the payment ACL. - Simulation (
lib/latchkey/simulation/) — the seeded catalogue, a deterministic world-line, tenant-behaviour profiles, and an Oban-driven sweep. "Now" is wall-clock time (Date.utc_today(), Australia/Sydney — ADR 0005): back-dated history is seeded and the sweep reveals events as they fall due, so histories accrue realistically rather than all at once. - Inspector (
/inspector,lib/latchkey_web/live/inspector/) — the app's front door: a LiveView for browsing the event log, per-tenancy streams, the ledger, and the timeline. It's the primary way to see the event sourcing at work.
Phoenix 1.8 · Ash 3 + AshPostgres (read model) · Commanded 1.4 + EventStore (write side) · Oban (simulated-time sweep) · PostgreSQL (Ecto/Postgrex) · Bandit · LiveView · Tailwind.
mix setup— install deps, provision the Ash and EventStore databases, build assets, seedmix phx.server(oriex -S mix phx.server) — start the server
Then visit localhost:4000 — the root redirects to
/inspector, the event-log front door. Start there to see the model running.
Run mix precommit before committing — it compiles with warnings-as-errors,
prunes unused deps, formats, and runs the tests.