Skip to content

Architecture Overview

The Tech Strategy Tool is built on an event-sourced architecture with a single API host serving two Blazor WebAssembly frontends. This page describes the high-level system design and the reasoning behind key architectural decisions.

Diagram

All GET requests are served from the event processor’s in-memory state — no database queries for current data. All writes are serialized through the processor via a semaphore lock, then persisted to PostgreSQL. SSE notifications flow back to all connected clients so they can re-fetch affected entities.

Event sourcing was chosen because history, restore, and conflict detection are first-class product features — not infrastructure afterthoughts.

  • History is a product feature. The per-entity history view, the re-apply mechanism, and full-strategy restore are core user-facing capabilities. In a CRUD system, these would require a bolted-on audit log, diff tracking, and snapshot/restore logic. With event sourcing, the event log IS the history, previous values are derived by replaying to any point, and restore is “rebuild the document at sequence N and make it current.”

  • Conflict detection for collaborative editing. The per-field sequence tracking (lastSeenSequence) that enables safe concurrent editing is a natural extension of the sequential event model. Each field knows which event last modified it, and stale edits are rejected with the current server value.

  • The domain is naturally event-shaped. Strategy changes are discrete, attributable actions: “Alice renamed a principle,” “Bob assigned a principle,” “Carol created an initiative.” These map directly to events. Cascading operations (like deleting a principle referenced by multiple objectives) are recorded as a single event with the processor handling all side effects — the event captures intent, the processor handles mechanics.

See Event Sourcing for the full event flow and processing model.

The system targets approximately 20 concurrent editors and 50 viewers. The architecture embraces this constraint:

  • A single in-memory document — the entire strategy is held in memory by the event processor
  • A single-writer processor — all events are serialized through a SemaphoreSlim(1,1) lock, eliminating distributed locking and transaction coordination
  • Deterministic event ordering — a single monotonic sequence number for all events
  • Fast reads — all GET requests served from in-memory state, no database round-trip

The trade-off is that the system runs as a single API server instance. This is a deliberate design choice — the tool is correct and simple at this scale, and complexity would only be added when a concrete need materializes.

The Strategy Site and Admin Site are separate Blazor WebAssembly applications with distinct concerns:

SiteURLAudiencePurpose
Strategy Site/strategyViewers, EditorsDay-to-day editing of principles, objectives, initiatives
Admin Site/adminAdministratorsUser management, team setup, event log, restore

Both are served by the same ASP.NET Core host and share the same API, cookie-based authentication, and SSE infrastructure. Keeping them separate means:

  • The Strategy Site is optimized for the editing workflow (low friction, meeting-friendly)
  • The Admin Site is a management console with different interaction patterns
  • The Strategy Site does not ship admin UI code to non-admin users
  • Both apps can evolve independently

They share a common foundation:

Diagram

Server-Sent Events push lightweight notifications to all connected clients. When an event is processed, the server broadcasts which entities were affected — entity type and ID only, no payloads. Clients then fetch fresh data for only the changed entities.

This approach:

  • Avoids pushing full document state on every change
  • Keeps SSE messages minimal (no data synchronization complexity)
  • Lets clients decide how to refresh (single card vs. full view reload)
  • Uses the browser’s native EventSource API with automatic reconnection

See Real-Time Updates (SSE) for protocol details.

A typical user action follows this path:

  1. The Blazor client submits an event via POST /api/events
  2. The API acquires the single-writer lock
  3. The event processor validates and applies the change in memory
  4. The resulting event(s) are persisted to PostgreSQL
  5. SSE notifications are broadcast to all connected clients (including the submitter)
  6. The submitting client receives the processing result
  7. All clients receive SSE notifications and re-fetch the affected entity from the API
  8. The API serves GET requests from the processor’s in-memory state (no database query)

On startup, the server reconstructs its in-memory state before accepting any requests:

  1. Load the latest checkpoint from the database (a serialized snapshot of the strategy document)
  2. Replay all events after the checkpoint’s sequence number
  3. Save a new checkpoint (ensures old non-decomposed events are never replayed again)
  4. Begin accepting requests

This process is handled by ProcessorInitializer, an IHostedService that runs before the app starts serving traffic.

FilePurpose
src/TechStrat.Core/Processing/EventProcessor.csCore event processing loop
src/TechStrat.Api/Endpoints/EventEndpoints.csEvent submission endpoint with single-writer lock
src/TechStrat.Api/Sse/SseConnectionManager.csSSE connection tracking and broadcast
src/TechStrat.Infrastructure/Persistence/EventStore.csEvent persistence
src/TechStrat.Api/Auth/SessionAuthenticationHandler.csCookie-based session auth