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.

System Architecture

graph LR
    subgraph Clients
        SS[Strategy Site<br/>Blazor WASM<br/>Viewers & Editors]
        AS[Admin Site<br/>Blazor WASM<br/>Administrators]
    end

    subgraph "API Host"
        API[ASP.NET Core<br/>Minimal API]
        EP[Event Processor<br/>Singleton · In-Memory]
        SSE[SSE Connection<br/>Manager · Singleton]
    end

    subgraph Persistence
        PG[(PostgreSQL 17<br/>Events · Checkpoints<br/>Users · Sessions)]
    end

    SS -->|"HTTP POST (events)"| API
    AS -->|"HTTP POST (events)"| API
    SS -->|"HTTP GET (reads)"| API
    AS -->|"HTTP GET (reads)"| API
    API -->|"Serialize writes"| EP
    API -->|"Persist"| PG
    EP -->|"Notifications"| SSE
    SSE -.->|"SSE push"| SS
    SSE -.->|"SSE push"| AS

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.

Design Principles

Event sourcing as a natural fit

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 changed the title," "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.

Not about scalability

This is not event sourcing for CQRS read/write separation or horizontal scaling. The single-writer, single-reader, in-memory model is intentionally simple. Event sourcing was chosen for domain fit, not infrastructure reasons.

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

Simplicity at the right scale

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.

Two frontends, one host

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

Site URL Audience Purpose
Strategy Site /strategy Viewers, Editors Day-to-day editing of principles, objectives, initiatives
Admin Site /admin Administrators User 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:

graph TD
    UI[TechStrat.UI<br/>Shared Components]
    Shared[TechStrat.Shared<br/>DTOs & Contracts]

    SS[Strategy Site] --> UI
    SS --> Shared
    AS[Admin Site] --> UI
    AS --> Shared

    UI --> Shared

Real-time updates via SSE

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.

Request Flow

A typical user action follows this path:

sequenceDiagram
    participant Browser as Blazor Client
    participant API as API Host
    participant EP as Event Processor
    participant DB as PostgreSQL
    participant SSE as SSE Manager

    Browser->>API: POST /api/events
    API->>API: Acquire SemaphoreSlim lock
    API->>EP: ProcessEvent(envelope)
    EP->>EP: Validate & apply
    EP-->>API: ProcessingResult
    API->>DB: Append event(s)
    API->>SSE: Broadcast notifications
    API-->>Browser: SubmitEventResponse
    SSE-->>Browser: SSE: card-changed / view-reload
    Browser->>API: GET /api/... (refresh data)
    API->>EP: Read from in-memory state
    EP-->>API: Current data
    API-->>Browser: JSON response
  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)

Cold Start and Recovery

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

sequenceDiagram
    participant Init as ProcessorInitializer
    participant DB as PostgreSQL
    participant EP as Event Processor

    Init->>DB: Load latest checkpoint
    DB-->>Init: Document snapshot + sequence number
    Init->>EP: LoadFromCheckpoint(document, seq)
    Init->>DB: Get events after checkpoint
    DB-->>Init: Events to replay
    loop Each event
        Init->>EP: ReplayEvent(event)
    end
    Init->>DB: Save new checkpoint
    Note over Init: Ready to accept 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.

Key Files

File Purpose
src/TechStrat.Core/Processing/EventProcessor.cs Core event processing loop
src/TechStrat.Api/Endpoints/EventEndpoints.cs Event submission endpoint with single-writer lock
src/TechStrat.Api/Sse/SseConnectionManager.cs SSE connection tracking and broadcast
src/TechStrat.Infrastructure/Persistence/EventStore.cs Event persistence
src/TechStrat.Api/Auth/SessionAuthenticationHandler.cs Cookie-based session auth

Further Reading