Data Model
The Tech Strategy Tool’s domain model is a hierarchy of strategy entities managed through an event-sourced processor. This page describes the entity relationships, storage model, and how the in-memory document relates to persistent storage.
Entity Hierarchy
Section titled “Entity Hierarchy”Entity Relationships
Section titled “Entity Relationships”Ownership vs. reference
Section titled “Ownership vs. reference”Solid lines in the diagram indicate ownership — the parent entity’s deletion cascades to the child. The Objective-to-Principle relationship is a reference — deleting a Principle removes the association but does not delete the Objective.
| Relationship | Type | Cascade behavior |
|---|---|---|
| Strategy -> Team | Ownership (ordered) | Deleting strategy removes all teams |
| Team -> Principle | Ownership (ordered) | Deleting team removes all principles |
| Team -> Group | Ownership (ordered) | Deleting team removes all groups |
| Team -> Objective | Ownership (ordered) | Deleting team removes all objectives |
| Objective -> Initiative | Ownership (ordered) | Deleting objective removes all initiatives |
| Objective -> Principle | Reference (many-to-many) | Deleting principle removes association only |
| Objective -> Group | Reference (optional) | Deleting group unassigns objectives (they become ungrouped) |
Domain Entities
Section titled “Domain Entities”Strategy
Section titled “Strategy”The root document. Contains an ordered list of teams. There is exactly one Strategy instance in the system, held in memory by the event processor.
A team represents an engineering group with its own strategy space. Teams are created and managed exclusively in the Admin Site.
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier |
| Name | string | Display name |
| Color | string | Hex color code (e.g., #3498db) — validated on create and update |
| Principles | List<Principle> | Ordered list of principles |
| Objectives | List<Objective> | Ordered list of objectives |
| Groups | List<Group> | Ordered list of groups for organizing objectives |
A grouping mechanism for objectives within a team. Groups provide visual organization in the Objectives view.
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier |
| Name | string | Display name |
| Description | string | Optional description |
Deleting a group unassigns its objectives (they become ungrouped and display under a default heading) but does not delete them.
Principle
Section titled “Principle”A guiding belief or standard that a team follows. Principles represent the “why” behind technology decisions.
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier |
| Name | string | Principle name (supports *highlight* syntax — highlighted text renders in the team color) |
| Description | string | Detailed description |
Cross-team sharing: When assigned to an Objective in a different team, an independent copy is created in the Objective’s team. The copy is a separate entity — there is no live link to the source. Each team can edit its copy independently.
Cascade on delete: Deleting a principle removes it from the PrincipleIds list of all objectives that reference it in the same team.
Objective
Section titled “Objective”A measurable goal aligned to one or more principles. Objectives track progress through their initiatives.
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier |
| Name | string | Objective name |
| GroupId | Guid? | Optional group assignment |
| PrincipleIds | List<Guid> | Linked principles (many-to-many by reference) |
| Initiatives | List<Initiative> | Ordered list of initiatives |
Total progress: Computed as the arithmetic mean of all child initiative progress values, rounded to the nearest integer. Returns 0% if the objective has no initiatives.
Principle linking: A principle can be associated with an objective only once — duplicate assignment is rejected.
Initiative
Section titled “Initiative”A concrete project or work item that drives progress on an objective.
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier |
| Name | string | Initiative name (plain text, no highlight syntax) |
| Progress | int | Completion percentage (0-100) |
| JiraKey | string? | Optional Jira issue key |
Deleting an initiative immediately recalculates the parent objective’s total progress.
Entity Identity
Section titled “Entity Identity”All entity IDs are globally unique GUIDs. The processor locates entities by ID alone without needing to know the parent hierarchy. This simplifies lookups for events like update_name where only the target entity’s ID is needed.
Ordering
Section titled “Ordering”All collections use List<T>, which preserves insertion order and supports insert-at-index operations:
- Principles within their Team
- Groups within their Team
- Objectives within their Group (or the ungrouped section)
- Initiatives within their Objective
Ordering is modified via reorder events (reorder_objective, reorder_initiative, reorder_group) that specify a target index. The UI exposes this through drag-and-drop.
Storage Model
Section titled “Storage Model”The domain model exists in two forms:
In-memory document
Section titled “In-memory document”The EventProcessor holds the current Strategy document in memory. This is the authoritative state for all reads. The document is a plain C# object graph with mutable properties and List<T> collections.
Persistent storage (PostgreSQL)
Section titled “Persistent storage (PostgreSQL)”The database stores the building blocks for reconstructing the in-memory document:
| Table | Purpose |
|---|---|
events | Append-only event log (JSONB data column) |
checkpoints | Periodic document snapshots (JSONB document_json column) |
history_associations | Maps events to all entities they affect (for entity history queries) |
users | User accounts with password hashes |
sessions | Active login sessions |
Database indexes
Section titled “Database indexes”| Table | Column | Type | Purpose |
|---|---|---|---|
events | sequence_number | Unique | Event ordering |
events | target_id | Non-unique | Entity history queries |
checkpoints | sequence_number | Unique | Checkpoint ordering |
history_associations | entity_id | Non-unique | Entity history queries |
history_associations | event_sequence | Non-unique | Event-based lookups and restore cleanup |
users | username | Unique | Login lookups |
sessions | user_id | Non-unique | Per-user session queries |
Serialization
Section titled “Serialization”Events
Section titled “Events”Event data is stored as a JSON dictionary in the data JSONB column. Each event type has its own set of expected keys:
// create_entity event data (Team — targetType: "Team" on the envelope){ "id": "a1b2c3d4-...", "name": "Platform Team", "color": "#3498db" }
// update_name event data (targetType: "Principle" on the envelope){ "name": "API-First Design" }Checkpoints
Section titled “Checkpoints”Checkpoints serialize the entire Strategy document as JSON using System.Text.Json. The JSON is stored in a JSONB column, which PostgreSQL normalizes (whitespace is stripped).
Further Reading
Section titled “Further Reading”- Entities — Detailed entity documentation with all fields and behaviors
- Event Processing — How events modify the domain model
- Event Types — Complete catalog of all event types