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¶
graph TD
S[Strategy Document] --> T1[Team]
S --> T2[Team]
T1 --> G1[Group]
T1 --> G2[Group]
T1 --> P1[Principle]
T1 --> P2[Principle]
T1 --> O1[Objective]
T1 --> O2[Objective]
O1 --> I1[Initiative]
O1 --> I2[Initiative]
O2 --> I3[Initiative]
O1 -.->|references| P1
O2 -.->|references| P1
O2 -.->|references| P2
O1 -.->|assigned to| G1
O2 -.->|assigned to| G2
Entity Relationships¶
erDiagram
STRATEGY ||--o{ TEAM : "contains (ordered)"
TEAM ||--o{ PRINCIPLE : "owns (ordered)"
TEAM ||--o{ GROUP : "owns (ordered)"
TEAM ||--o{ OBJECTIVE : "owns (ordered)"
GROUP ||--o{ OBJECTIVE : "organizes (optional)"
OBJECTIVE }o--o{ PRINCIPLE : "references"
OBJECTIVE ||--o{ INITIATIVE : "contains (ordered)"
TEAM {
guid id
string name
string color
}
PRINCIPLE {
guid id
string title
string description
}
GROUP {
guid id
string name
string description
}
OBJECTIVE {
guid id
string title
int totalProgress
}
INITIATIVE {
guid id
string name
int progress
string jiraKey
}
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¶
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.
Team¶
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 |
Group¶
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¶
A guiding belief or standard that a team follows. Principles represent the "why" behind technology decisions.
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier |
| Title | string | Principle title (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¶
A measurable goal aligned to one or more principles. Objectives track progress through their initiatives.
| Field | Type | Description |
|---|---|---|
| Id | Guid | Unique identifier |
| Title | string | Objective title |
| Description | string | Detailed description |
| 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¶
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¶
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_principle_title where only the principle ID is needed.
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¶
The domain model exists in two forms:
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)¶
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 |
No entity tables
There are no tables for teams, principles, objectives, etc. The current state is always derived from events. The checkpoints table stores serialized snapshots of the entire strategy document for efficient cold start.
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¶
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_team event data
{ "id": "a1b2c3d4-...", "name": "Platform Team", "color": "#3498db" }
// update_principle_title event data
{ "title": "API-First Design" }
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¶
- Entities — Detailed entity documentation with all fields and behaviors
- Event Processing — How events modify the domain model
- Event Types — Complete catalog of the 28 event types