Case study 01 · Events & data
Event-sourced reporting across a whole business
- Context
- A multi-tenant business platform, built for a client
- My role
- One of the engineers on the reporting domain. I built projections, aggregations, dashboards and operational reporting on an event store a colleague set up.
- When
- 2025 – early 2026
- Stack
- .NET · Marten · PostgreSQL · Elasticsearch · Azure Service Bus · FastEndpoints
The problem
The platform was split into domains: stock, leads, sales, pricing, integrations. Each one recorded what happened to it as events. The business wanted one view across all of them: dashboards and snapshots that summarise the whole operation, and operational tables people could filter and export. It had to be fast, it had to be correct per tenant, and it had to survive the calculations changing.
What I did
- Wrote async projections that fold events from several streams into one document per business key. A stock item’s view, for example, combines stock, deal, location and listing events.
- Built the dashboard snapshot configuration and around ten Elasticsearch aggregations behind it: active leads and tasks per person, stock by location and status, lead conversion, gross profit, listing coverage per sales channel, price-change counts.
- Built configurable “operations” tables over projected data, with filter columns, display names and lookups generated from shared metadata.
- Added projections and event-contract changes for an accounting integration, and a rebuild that runs off a bus message so a long replay never holds an HTTP request open.
Decisions and tradeoffs
- Two stores, two jobs. PostgreSQL read models hold state that can be rebuilt from history. Elasticsearch, with an index per tenant, does the fast bucketing dashboards need. Neither is asked to do the other’s job.
- Eventually consistent, on purpose. Dashboards lag the write side by a moment. That is fine for reporting, but tests have to wait for projections to catch up before they assert.
- Replay instead of patch. Because events are the record, a changed calculation means rewinding a projection and replaying it, not hand-fixing rows. Creates have to be idempotent so a replay gives the same answer twice.
- Names are a contract. Projection type names are stored with their progress, so renaming one quietly breaks replay. Refactors have to respect that.
What I took from it
This is where I learned the most. Event sourcing is not the same as publishing messages. The events are the source of truth, and everything you read is a view you can throw away and rebuild.
my partexisting
Switch on “Show working” for the annotations.