Skip to content

Domain model map

One visual reference for contexts, aggregates, entities, and value objects.

Tags: Bounded Contexts, Aggregates, Entities, Value Objects

Area: CoachMe DDD relationship map

This page is the source-of-truth graph for bounded-context ownership, aggregate roots, and cross-context references. As new feature pages are designed, we can add focused domain-map pages and link them back here.

Bounded contexts

The ten contexts below match docs/doc-map.md section 2 and the apps/api/src/<context> layout. Each can later get its own graph page when its aggregates become too dense.

Status here describes documentation depth, not build state — feature code is still at zero per the development readiness review. "Specced" means the context has feature technical pages that define its aggregates.

IdentityAccess

Specced. Owns users, auth identities, app sessions, provider availability, provider policy, and provider audit history. Its identity and membership aggregates sit outside the tenant fence — see Tenant scope.

CoachClientManagement

Specced. Owns client profiles, relationship-scoped requirements, readiness reviews, notes, onboarding workflows, measurement plans, progress entries, and photo evidence.

Forms

Specced. Owns questionnaire templates, versions, assignments, schedules, submissions, and review tasks. See questionnaire-builder, media-form-attachments.

ProgramsAndExercise

Specced. Owns the exercise catalog and taxonomy, program templates, drafts, published versions, assigned programs, and program days. See program-builder, exercise-library.

DailyExecution

Specced. Owns Today items, quick logs, workout execution state, session logs, set-level RPE/RIR, the offline submission outbox, and adherence signals. See today-view, workout-logging, offline-workout-mode.

Nutrition

Specced. Owns the nutrient and food catalog, nutrition plan templates, drafts and published versions, relationship targets, daily intake logs, meal swap requests, and supplement schedules. Part of the Core Coaching + Nutrition Baseline for Public V1. See the four nutrition-* feature pages.

Communication

Specced. Owns conversation threads, messages, voice-note assets, notification intents and deliveries, and push installations. See in-app-messaging, voice-notes, push-notifications.

ProgressAnalytics

Specced. Consumes events and builds read models for timelines, charts, PRs, effort trends, comparisons, adherence scoring, risk flags, and weekly recaps. Owns no write model of its own. See client-risk-flags, priority-view, coach-task-inbox.

Integrations

Specced. Owns provider adapters and their configuration, credential references, and sync policy. Contexts consume providers through this boundary rather than calling them directly. See ymove-exercise-integration and the integration register.

Billing

Post-V1. No aggregates defined yet; listed so the map stays complete against doc-map.

Aggregate and entity relations

These relation statements are intentionally explicit so they can become tests, database constraints, and API authorization rules later.

  • User → owns many AuthIdentity records; uniqueness is scoped by provider, issuer, tenant, and provider subject.
  • User → owns one CoachProfile when the user has the coach role and completed registration.
  • CoachWorkspace → contains WorkspaceMembership records; the first coach registration creates an OWNER membership.
  • ClientProfile → owns profile sections for goals, health, nutrition, training, preferences, and locale/timezone baseline.
  • ProfileRequirementSet → belongs to a coach-client relationship and decides which profile sections must be complete before review.
  • ClientProfileReview → records relationship-specific readiness; profile completion does not equal coach approval.
  • OnboardingWorkflowDefinition → stores the system-defined V1 baseline flow used by client invitations and relationship-scoped workflow instances.
  • OnboardingWorkflowInstance → contains ordered OnboardingTask records and references, but does not own, form submissions, profile sections, measurements, or photos.
  • OnboardingOverride → records coach/system cancellation, reassignment, nudges, or force-complete decisions as append-only audit facts.
  • FormTemplate → owns draft form metadata, while FormVersion freezes the published question definition.
  • FormAssignment → targets one relationship and one published form version; FormSchedule generates repeated assignments.
  • FormSubmission → stores answers for a specific assignment/version and can apply explicit FormFieldMapping rules.
  • MeasurementPlan → owns required MeasurementPlanItem rules and one active plan exists per relationship.
  • MeasurementEntry → contains many MeasurementValue values and may link to a check-in/task submission.
  • ProgressPhotoSet → contains angle-specific ProgressPhotoAsset records stored through the shared object-storage layer.
  • WorkoutSession → contains performed exercise and set logs, including submitted load, reps, notes, and RPE/RIR readings.
  • PersonalRecordEvent → is projected from submitted workout logs and feeds CurrentPersonalRecord read models.
  • ProgramTemplate → owns reusable program identity; ProgramDraft is the mutable builder and ProgramVersion the immutable published snapshot assigned to relationships.
  • AssignedProgram → references one published ProgramVersion by id and contains generated ProgramDay records; editing a template never mutates an active assignment.
  • ExerciseCatalogItem → spans the global free-exercise-db import and coach-authored custom exercises; the imported rows are un-fenced, the custom rows are tenant-scoped.
  • NutritionPlanTemplate → owns reusable plan identity, mirroring the template/draft/version split used by programs.
  • NutritionPlanVersion → freezes meal slots, planned portions, alternatives, and supplement links; DailyNutritionLog references it by id rather than embedding it.
  • NutritionTargetSet → owns relationship-scoped targets and day-type mappings; exactly one set is effective per relationship at a time.
  • MealSwapRequest → records a client request against a specific plan version and resolves through an explicit coach decision; approval may or may not update the plan.
  • ConversationThread → is relationship-scoped and contains ConversationMessage records; voice notes are a message kind, not a separate timeline.
  • NotificationIntent → is a provider-neutral request to notify one recipient about one source event; NotificationDelivery records one attempt through one provider to one PushInstallation.
  • ExerciseProviderIntegration → owns provider configuration and credential references; contexts reach providers through this boundary rather than calling them directly.

Tenant scope

multi-tenancy owns the tenant fence and defines four table classes; it delegates aggregate ownership to this page, and its foreign-key rule follows the ownership verbs above. This section is the join between the two: it declares which class each aggregate root falls in, using that page's vocabulary unchanged.

Tenant-scoped is the default. An aggregate is listed below only if it is not tenant-scoped, or if it needs an argument for why it is.

Aggregate rootClassWhy
User, AuthIdentity, CoachWorkspace, WorkspaceMembershipIdentity and membershipRead before a workspace is known, to answer which workspaces the user may select. Fencing them by workspace would make context selection impossible.
ExerciseCatalogItem, ExerciseTaxonomyNodeGlobal referenceThe approved free-exercise-db import and its taxonomy, un-fenced and readable by all tenants. Coach-authored custom exercises are tenant-scoped and live alongside it.
OnboardingWorkflowDefinitionGlobal referenceThe system-defined V1 baseline flow, written by migration and read by every workspace. Its OnboardingWorkflowInstance records are tenant-scoped.
OnboardingOverrideSystem and operationalAppend-only audit facts, reached only under an explicit support context.
NotificationIntent, NotificationDelivery, PushReceiptSystem and operationalCarry workspace_id so workers can route and scope, but are never reached ambiently from a request.

Everything else on this page is tenant-scoped: client profiles, relationships, programs, assignments, logs, forms, messages, media metadata, notes, and tasks all carry workspace_id and are reachable only through a tenant-scoped repository.

Open scope questions

These aggregates do not fall cleanly into one class. Per docs/doc-map.md, they stay open here rather than being resolved to match an assumption.

  • FoodItem, NutrientDefinition, and SupplementDefinition each describe both a system-seeded catalog row and a workspace- or coach-owned row. That spans global reference and tenant-scoped in one aggregate. Either it splits into two tables, or the seeded rows carry a nullable workspace_id and lose the structural guarantee. multi-tenancy requires every table to declare its class in review; these three cannot until this is decided.
  • AuthProviderConfig is provider policy read at login, before a workspace is resolved. It is probably global reference, but it is written by admins rather than by migration, which does not match that class's write rule.
  • PushInstallation binds a user and an app surface, not a relationship. Whether it is identity and membership or tenant-scoped depends on whether a user with two workspaces needs separate installations.

Value objects and invariants

Value objects are grouped by the aggregate or boundary where they enforce rules. This section should grow with each new feature page.

Identity value objects

  • ProviderRef: provider, issuer, tenant, subject, verification metadata.
  • EmailAddress and PhoneNumber: normalized before uniqueness checks.
  • TermsAcceptance: immutable accepted version, timestamp, IP hash, and user agent.

Provider policy objects

  • ProviderKey: stable internal provider key.
  • ProductSurface: WEB, COACH_MOBILE, CLIENT_MOBILE.
  • VerificationPolicy: verified email, phone, or either.

Profile value objects

  • ProfileSectionKey: personal, goals, health, nutrition, training, preferences.
  • FieldRequirement: requirement rule, source, visibility, validation type.
  • ProfileReadinessStatus: review and programming state.

Onboarding value objects

  • OnboardingTaskType: V1 baseline uses questionnaire; future types can add profile, consent, resource, measurement, and photo tasks.
  • OnboardingTaskStatus: ready, opened, skipped for now, completed, cancelled, expired.
  • WorkflowStatus: in progress, completed, completed with pending task, paused, cancelled.

Progress value objects

  • MeasurementKind: body weight, circumference, body composition, performance, custom.
  • MeasurementUnit: submitted and canonical units.
  • PhotoAngle: front, side, back, custom.

Progress invariants

  • Submitted entries are corrected by versioning, not edited in place after coach review.
  • Photo assets are hidden until scan, metadata stripping, thumbnail generation, and permissions pass.
  • Progress date and upload timestamp are separate business facts.

Workout execution values

  • EffortMetric: none, RPE, or RIR.
  • EffortReading: validated effort value captured from client logs.
  • PerformedLoad: submitted unit plus canonical load for comparison.

Performance values

  • PersonalRecordType: max load, estimated 1RM, reps at load, volume.
  • ExerciseRecordScope: relationship plus performed exercise identity.
  • EffortTrendWindow: weekly or program-block rollup period.

Cross-context references

  • Use IDs and events across contexts, not object ownership.
  • Downstream contexts consume facts from profiles and progress evidence.
  • Relationship access is enforced in use cases and API guards.

Relationship graph

The graphs show ownership first, then the most important cross-context references and event flows. Arrows point from owner or producer to dependent model or consumer. The Mermaid source is canonical; edit it directly in this page.

Read the context graph for "what talks to what", then the per-context graphs for "what owns what" inside one boundary.

Legend

Node kind is carried by shape, so labels stay short enough to read:

ShapeMermaidKind
Double-bordered box[[X]]Bounded context
Rectangle[X]Aggregate root — consistency and transaction boundary
Rounded box([X])Entity owned by an aggregate root, no independent lifecycle
Cylinder[(X)]Read model — a projection, never written to directly
HexagonValue object or enum that enforces a rule

Edge kind is carried by line style:

  • Solid arrow — ownership or containment, inside one aggregate or context.
  • Dashed arrow — a reference by id, an emitted event, or a read-model feed.

Keep the diagrams legible

Mermaid scales each diagram down to fit the prose column, so a wide graph becomes unreadable text rather than a scrollbar. Two rules keep that from happening: use flowchart TD (top-down grows down the page, not across it), and never put the node kind in the label — that is what the shapes are for. A graph past roughly 12 nodes should be split, not shrunk.

Context graph

The cross-context view: what talks to what. Solid arrows are the flow of work through the product; dashed arrows are event or read-model feeds. No aggregate detail here — that is the next section.

Billing is drawn unconnected on purpose: it is Post-V1 and its relationships are not yet decided. An edge here would assert a contract that no feature page defines.

Per-context aggregate graphs

One graph per boundary, each small enough to read. Cross-context references are deliberately absent here — they live in the context graph and in Cross-aggregate references below — so that each graph answers exactly one question: what does this context own, and how does it hang together?

When a context outgrows its graph, promote it to a child page and link it from here.

IdentityAccess

CoachClientManagement — client profiles

Requirements, reviews, and notes all reference the profile rather than nest inside it: a profile is one client's data, while what is required and whether it is approved are facts about a coach-client relationship.

CoachClientManagement — onboarding

CoachClientManagement — progress evidence

Forms

ProgramsAndExercise

Nutrition — plan authoring

Nutrition mirrors the template/draft/version split used by programs: a mutable draft, then an immutable published version that assignments reference.

Nutrition — client execution

NutritionPlanVersion is the hub: logs, swaps, and supplement rows all reference the published prescription rather than copy it.

DailyExecution and ProgressAnalytics

ProgressAnalytics owns no write model. Everything it holds is a projection, which is why its nodes are cylinders and its only inbound edges are builds read model from.

Communication and Integrations

Cross-aggregate references

These are the edges that cross an aggregate or context boundary. They are listed rather than drawn, because drawing them is what turned this page into an unreadable schema diagram. Each one is a plain id column with no foreign key, resolved by a tenant-scoped read — see Prefer language precision.

HolderReferences by idWhy it crosses
ProfileRequirementSetFormVersionRequirements decide which published form satisfies a section.
OnboardingTaskFormAssignmentA task points at the assignment it opens; it does not own the submission.
FormSubmissionClientProfileFormFieldMapping writes answers into profile sections through an event, not a join.
ExerciseCatalogItemExerciseProviderIntegrationCatalog rows record which provider supplied them.
WorkoutSessionAssignedProgramExecution references the assigned snapshot it was performed against.
NotificationIntentany source aggregateIntents name a source event, staying provider- and context-neutral.

Domain events

Every dashed emits edge above is carried by a published event, but this page does not name events. event-and-outbox-standard owns event naming, the envelope, versioning, and outbox behavior; duplicating names here would create a second source of truth that drifts.

What this page contributes is the shape: an event is emitted by the aggregate root that owns the fact, and consumed by another context that must not reach into that aggregate. Read an emits edge as "the owner publishes a fact; the consumer subscribes" — never as a synchronous call.

Names follow context.aggregate.fact.v1, as in client.relationship.activated.v1 or nutrition.meal_swap.approved.v1. When a feature page defines a new event, add the edge here and the name there.

How to use this source

  • Keep names aligned with feature technical pages.
  • Add each new feature's aggregate root as soon as the feature page gets a Domain Model section.
  • Put cross-context flow in the context graph and aggregate internals in that context's own graph. Never add aggregates to the context graph.
  • Carry node kind in the shape, never in the label. A label is a name and nothing else.
  • Use flowchart TD. Left-to-right graphs grow wider than the prose column and get scaled into illegibility.
  • List a boundary-crossing reference in Cross-aggregate references rather than drawing it into a per-context graph.
  • Use solid arrows for ownership and dashed arrows for events, read-model feeds, or references by id.
  • Declare the tenant class of every new aggregate root in Tenant scope, or record why it is undecided.
  • If a context passes roughly 12 nodes, split it — a second graph here, or a child page.

Growth rules for future pages

As CoachMe grows, this page remains the overview and individual contexts get deeper graph pages.

One overview map

Keep this page focused on cross-domain understanding. It should answer, “what owns what, and what talks to what?” without becoming a full schema diagram.

Many focused child pages

Create pages such as /features/domain-identity-access, /features/domain-client-management, and /features/domain-programs-exercise when a context needs detailed aggregate internals.

Every feature contributes

When a feature technical page defines aggregates, entities, value objects, events, repositories, or external ports, add the changed nodes and edges here.

Prefer language precision

The aggregate relation graph uses a closed set of verbs, because each one has a consequence in code:

VerbMeansConsequence
ownsA bounded context is the write authority for this aggregate root.Only this context's use cases may mutate it.
containsAn entity or value object inside an aggregate boundary.Same transaction, same repository, cascades with the root. A foreign key is allowed here.
references by idA pointer to an aggregate the holder does not own.Store a plain id. No navigation, no cascade, and per multi-tenancy no foreign key across an aggregate or context boundary — the reference is resolved by a tenant-scoped read and validated as an invariant.
emitsThe owner publishes a fact after commit.Goes through the outbox; consumers are async and idempotent.
consumesA context subscribes to another's fact.Never a synchronous call into the producer.
builds read model fromA projection derived from events.Rebuildable, never written to directly.

If an edge needs a verb outside this set, that usually means the relationship is not yet understood — say it in prose in Aggregate and entity relations first, and add the edge once it resolves to one of the six.

The context graph is the exception: it is a narrative overview, so descriptive edge labels like "supplies assigned snapshots to" are fine there. Precision is enforced where it has consequences.

CoachMe internal planning documentation.