Stack
Next.js 15 · React 19 · ASP.NET Core 9 · PostgreSQL · Marten (event store)
Event Sourcing Architecture
- Designed and implemented an event-sourced backend using Marten on PostgreSQL, where all state changes are persisted as immutable domain events rather than mutable rows
- Aggregates replay from events at runtime, giving the system a full audit trail and the ability to time-travel to any historical state
- Async projection daemons rebuild read models in the background, keeping query-side views eventually consistent without blocking the write path
Configurable Workflow Engine
- Built a multi-step workflow engine where admins define steps, field definitions, and transition rules through a configuration UI — no code changes required to ship new approval flows
- Each workflow instance tracks field values, designer tasks, internal drafts, and approval decisions across an arbitrary number of steps
- Workflow versions are immutable; in-flight jobs always run against the version they were started on, preventing mid-flight schema drift
DashboardJobView — Single-Hop Read Model
- Designed and implemented a MultiStream projection (DashboardJobView) that aggregates job state, current step, assignee, and status into a single denormalised Marten document
- Eliminated a 3-hop database query (jobs → step_instances → assignments) previously required to render the dashboard, replacing it with a single document fetch
- Projection is maintained in real time by an async daemon, so the dashboard always reflects the latest job state without on-demand joins
Slim StepInstances Optimization
- Identified that DashboardJobView documents were storing full embedded arrays of StepInstance objects, causing documents to balloon as workflows added steps and fields
- Replaced the StepInstances dictionary with four slim denormalised fields (CurrentStepId, CurrentStepName, CurrentAssigneeId, CurrentAssigneeName), cutting per-document payload to only what the dashboard actually renders
- Achieved an estimated 60–85% reduction in per-document size for workflows with 5+ steps, improving Marten fetch and serialisation throughput proportionally
- Implemented a ToColumnKey() helper to normalise step and field identifiers into stable, collision-resistant keys for cross-version projection safety
- Maintained backwards compatibility: StepInstances is synthetically reconstructed from the slim fields on read for any consumers that still depend on the legacy shape