Skip to content

ConnectSoft SaaS — Aggregates, Entities, VOs & Enums Specification

This document is the canonical, implementation-ready specification of the ConnectSoft SaaS domain model. It defines aggregate boundaries, relationship semantics, property-level documentation, and ORM mapping guidance so teams can generate consistent code, migrations, and contracts across services.


Document conventions (DDD + ORM mapping)

Purpose

Set the rules of the road for how we model and document our domain. These conventions apply to every type that follows (aggregates, entities, value objects, enumerations) and to every relationship we declare (Reference, HasMany, HasManyThrough). They are intentionally domain-driven (behavior/invariants first) and ORM-aware (clear persistence shape without leaking database concerns into the model).


Aggregate boundaries

  • Aggregate Root (AR): The only entry point for invariants and mutations. Commands target a single AR.
  • Consistency: Strong inside an aggregate; eventual across aggregates via domain events and process managers.
  • Cross-context references: Never navigate across bounded contexts. Use identifiers/keys and integration events.

Rule of thumb: “One command → one aggregate.” If you need to touch two aggregates, raise a domain event and coordinate with a policy/saga.


Relationships vocabulary

We use three first-class relationship types. Each type must be explicitly documented when it appears in a model.

  • Reference Single navigation to a peer within the same bounded context. Stored as a foreign key (FK) plus an optional navigation property. Example: Edition has a Reference to its owning Product.

  • HasMany Collection of child entities or peer aggregates within the same context. Ownership and cascade rules must be explicit. Example: Product HasMany Edition.

  • HasManyThrough Many-to-many via an explicit link entity (join with behavior, invariants, effective-dating, etc.). Never implicit M:N. Example: Edition HasManyThrough EditionFeatureFeature.

Across contexts: Only IDs/keys are allowed (no navigations). E.g., a Billing rule may carry a FeatureKey string, not an IFeature navigation.


When to use navigation vs. IDs

Situation Use Why
Same bounded context, invariants span the relation Navigation (+ FK) Enables transactional invariants and expressive code
Same context, read-mostly, no invariant coupling ID-only (+ explicit query) Minimizes loading / N+1, keeps AR smaller
Different bounded context ID/Key only Prevents model leakage and accidental cross-context joins

Naming:

  • FK fields: <TargetName>Id (e.g., ProductId), keys: <TargetName>Key (e.g., FeatureKey).
  • Collections: pluralized (e.g., Editions, EditionFeatures).

Property documentation style (XML comments)

Every property in this spec will be documented as if it were a C# interface/class member. Use this pattern:

/// <summary>
/// Human-friendly name shown in administration UIs.
/// </summary>
/// <remarks>
/// Must be 1–200 UTF-8 characters. Not required to be unique.
/// </remarks>
string DisplayName { get; }

/// <summary>
/// Canonical key (slug) that uniquely identifies the entity within its context.
/// </summary>
/// <value>Lowercase, 3–64 chars: [a–z0–9][a–z0–9._-]+</value>
/// <remarks>
/// Immutable after first <c>Published</c> status. Used by tokens, entitlements, and APIs.
/// </remarks>
string Key { get; }

Where to put invariants:

  • If the invariant is property-level (e.g., “Key is immutable after publish”), include it in the property’s <remarks>.
  • If it’s type-level (e.g., “Product cannot publish without at least one published Edition”), put it in the type’s <remarks> block.

Value Objects (VOs)

  • Definition: Immutable, equality-by-value, owned by an aggregate. No identity.
  • ORM: Map as EF Core owned types. No separate tables unless the VO is large or repeated broadly.
  • Construction: Validate invariants at creation (factory methods).
  • Examples: ProductKey, FeatureKey, Quota, TenantRegionResidency.

Template

/// <summary>Canonical feature key.</summary>
/// <remarks>Global uniqueness across Catalog. Immutable after publish.</remarks>
public sealed record FeatureKey
{
    public string Value { get; init; }
    // Validation rules documented in spec; enforced in factory at runtime.
}

Entities (non-root)

  • Have identity (usually a Guid), live inside an aggregate root.
  • Use for child collections that have lifecycle of their own (e.g., Contact inside Tenant).
  • ORM: Same table as AR by default; split table only when size/volatility warrants.

Enumerations

  • Prefer smart enums or exhaustive static catalogs (no “magic ints”).
  • Each member in this document is listed with: Name, Value, ShortName, and clear semantics.
  • Versioning: Additive-first. Removing/renaming members requires a migration note and mapping guidance.

Event conventions (for reference in later cycles)

  • Name: <noun>.<pastParticiple> (e.g., product.published, edition.changed).
  • Envelope fields: eventId, occurredAtUtc, aggregateId, aggregateVersion, tenantId?, schemaVersion.
  • Evolution: Additive fields are allowed; breaking changes publish a new schemaVersion or topic.

EF-friendly mapping guidance

  • Owned VOs: Map with OwnsOne/OwnsMany. No separate identity.
  • Link entities: Always explicit for M:N (e.g., EditionFeature). Use composite keys and effective-dating where applicable.
  • Shadow FKs: Allowed to reduce public API noise (e.g., keep ProductId as FK while exposing Product navigation).
  • Loading strategy:
    • Command handlers: load only what you need (explicit includes).
    • Queries/reads: project to DTOs; prefer AsNoTracking.
    • Avoid global lazy loading (risk of N+1 and hidden I/O); use explicit Include/ThenInclude or separate queries.
  • Indexes & constraints:
    • Unique index on Key (per context) and on join pairs (e.g., (EditionId, FeatureId)).
    • Add filtered unique indexes for effective-dated records if you allow overlap rules.
  • Concurrency: Prefer optimistic concurrency with a rowversion/xmin column on aggregates.
  • Soft delete vs. lifecycle: Prefer lifecycle status (e.g., Deprecated, Retired) over hard deletes; actual data removal is a policy event (e.g., archival).

Relationship examples (mapping sketch)

// Product 1..* Edition
modelBuilder.Entity<Product>()
    .HasMany(p => p.Editions)
    .WithOne(e => e.Product)
    .HasForeignKey(e => e.ProductId)
    .IsRequired();

// Edition M..N Feature via EditionFeature
modelBuilder.Entity<EditionFeature>()
    .HasKey(x => new { x.EditionId, x.FeatureId });

modelBuilder.Entity<EditionFeature>()
    .HasOne(x => x.Edition)
    .WithMany(e => e.EditionFeatures)
    .HasForeignKey(x => x.EditionId);

modelBuilder.Entity<EditionFeature>()
    .HasOne(x => x.Feature)
    .WithMany(f => f.EditionLinks)
    .HasForeignKey(x => x.FeatureId);

Naming & files

  • Interface-first: We document interfaces that mirror aggregates/entities/VOs. Concrete classes may add behavior but must not change contract meanings.
  • Namespaces by context: ConnectSoft.Saas.Catalog, ConnectSoft.Saas.Billing, ConnectSoft.Saas.Identity, ConnectSoft.Saas.Tenants, etc.
  • File names: One type per file, matching type name. For link entities, use the pair name (e.g., EditionFeature).

Validation & invariants (how we’ll write them)

  • Preconditions at factory/constructor: mandatory fields, key format, status defaults.
  • State-change guards on AR methods: e.g., prohibit publishing a Product without at least one Published Edition.
  • Cross-field invariants: e.g., MinimumPrice ≤ BasePrice ≤ MaximumPrice.
  • Temporal invariants: effective date ≤ expiry date; no overlapping active windows unless explicitly allowed.

Read/write separation (at the doc level)

  • Write model: Aggregates, entities, VOs, invariants, and events (this spec).
  • Read model: Projections/DTOs derived from events/queries. We will annotate places where a read model is recommended instead of exposing deep navigations.

Cross-cutting enumerations: Access & Authorization

AccessScopeEnumeration

Describes where a rule applies (the target scope for access decisions).

Per-member properties (documented for each row below):

  • Name — Human-friendly label (shown in admin UIs).
  • ShortName — Slug used in policies/tokens (lowercase, _ for spaces).
  • Value — Stable integer for storage/wire contracts.
  • Description — Purpose, typical usage in policies, and scope boundaries.
Name (ShortName) Value Description (purpose, typical usage, scope boundaries)
Global (global) 1 Cross-platform scope affecting the entire system. Use for platform-wide kill switches, incident controls, and cross-tenant admin operations. Boundary: must not couple to tenant/edition specifics; apply only in platform admin planes.
System (system) 2 System-level operations and infrastructure controls (e.g., key rotation, provider credentials). Typical in SRE/ops policies. Boundary: restricted to internal principals; never used to grant tenant-facing features.
Feature (feature) 3 A single capability/module (e.g., “webhooks”). Use to gate visibility and actions within one feature. Boundary: do not leak to unrelated services; pair with FeatureKey.
Service (service) 4 A product-aligned service or set of APIs. Use to toggle service endpoints or admin functions for that service. Boundary: stays within the service’s bounded context.
Edition (edition) 5 Edition-bound access (Free/Standard/Enterprise). Use to enforce entitlements derived from Catalog. Boundary: never cross-edit ion—token/claims must carry the active edition.
Tenant (tenant) 6 Per-tenant controls (suspension, support access). Use in ABAC where tenantId is present. Boundary: RLS/filters must apply; no cross-tenant effects.
User (user) 7 Subject-specific overrides (e.g., preview access for a user). Boundary: scoped to the subject; do not imply group/project rights.
Group (group) 8 Role/group-based grants (e.g., “SupportAgents”). Boundary: effective only through group membership resolution in Identity.
Project (project) 9 Workspace/project resources. Use where tenants host multiple projects. Boundary: all checks must also satisfy tenant scoping.
Application (application) 10 Client/app-level rights (first- or third-party apps). Use to scope OAuth clients/service principals. Boundary: never bypass user/tenant checks.
Resource (resource) 11 Specific objects (files, records, datasets). Use for row/document-level ACLs. Boundary: must evaluate resource ownership and classification.
Custom (custom) 12 Extension point for domain-specific scopes. Use sparingly with a well-defined published contract and audit trail. Boundary: must be registered and discoverable in policy catalogs.

Linkage points

  • Used by IAccessRule and IAccessExceptionRule to declare where a rule applies.
  • Referenced from Edition/Feature rules (e.g., edition-scoped preview access).
  • Propagated into tokens/claims only as short names to keep payloads compact.

AccessTypeEnumeration

Describes what an actor may do at a scope (the action/permission flavor).

Per-member properties (documented for each row below):

  • Name — Human-friendly label (shown in admin UIs).
  • ShortName — Slug used in policies/tokens (lowercase, _ for spaces).
  • Value — Stable integer for storage/wire contracts.
  • Description — Purpose, typical usage in policies, and scope boundaries.
Name (ShortName) Value Description (purpose, typical usage, scope boundaries)
Read (read) 1 View-only. Typical for dashboards, audit log viewing, and GET APIs. Boundary: never implies modify or export; pair with data-class filters.
Write (write) 2 Create/modify. Used for POST/PUT/PATCH in admin/user planes. Boundary: must pass ABAC checks (edition/residency/ownership).
Execute (execute) 3 Run operations or jobs (e.g., replay webhooks, run exports). Boundary: side effects must be auditable and idempotent.
Admin (admin) 4 Administrative actions within a scope (not platform-wide). Boundary: limited to the owning context; often requires MFA and stronger audit.
Delete (delete) 5 Remove or hard-delete entities. Boundary: subject to retention/legal hold; prefer lifecycle states (Deprecated/Retired) in Catalog.
Modify (modify) 6 Change configuration/settings without creating new resources. Boundary: gated by change management; often distinct from write on data.
Share (share) 7 Grant/alter sharing of resources (invites, link generation). Boundary: must honor consent and preference policies.
Manage (manage) 8 Operate non-destructive admin tasks (rotate keys, manage seats). Boundary: narrower than admin; no schema changes.
Audit (audit) 9 Access to view/generate audit artifacts. Boundary: read-only; subject to data-class redaction and purpose-based access.
Approve (approve) 10 Approve/reject requests (e.g., quota increase, PR-based changes). Boundary: requires reviewer policy and traceable sign-off.
Configure (configure) 11 Configure options/experiments/flags. Boundary: cannot escalate identity roles or billing plans.
Access Control (access_control) 12 Define/manage access policies and role bindings. Boundary: meta-privilege—must be isolated, logged, and often requires dual control.

Linkage points

  • Evaluated by Identity (RBAC) and Config (ABAC predicates) on each request.
  • Bound into token claims (scopes/roles) as short names; downstream services evaluate against their published policy matrix.
  • Edition-aware policies combine AccessType with AccessScope and entitlements to yield effective permissions.

Notes & guidance

  • Additive-first: introduce new scopes/types as additions; existing consumers must ignore unknown values.
  • Granularity: prefer fewer, well-defined AccessType values, and use AccessScope to localize where they apply.
  • Auditing: every administrative AccessType (admin, manage, access_control, approve, configure) should produce structured audit records with correlation IDs.

Cross-cutting enumerations: Limits & Windows

LimitTypeEnumeration

Defines what dimension is being limited or metered (the “what” of quotas/usage).

Per-member properties shown in the table:

  • Name / ShortName — Display labels as defined in the enum.
  • Value — Stable integer for storage/wire contracts.
  • Unit (suggested) — Typical measurement unit used by metering/billing.
  • Meter Key (suggested) — Canonical snake_case key for counters.
  • Description — Purpose and typical usage.

The following members reflect the current code list. (Additions should be made additively to preserve compatibility.)

Name (ShortName) Value Unit (suggested) Meter Key (suggested) Description
API Calls 1 count api_calls Number of API invocations within a period; distinct from service-specific request tallies.
Storage 2 bytes/GB storage_bytes Total stored data footprint; often surfaced in GB for UI, billed from bytes.
Users 3 count users_count Number of provisioned identities/seats eligible to sign in.
Transactions 4 count transactions_count Discrete business operations (e.g., financial ops, writes).
Bandwidth 5 bytes/GB egress_bytes Network egress/transfer consumed.
Requests 6 count requests_count Service/endpoint request count (if tracked separate from API Calls).
Messages 7 count messages_count Communications/items processed (emails, chat messages, events).
Seats 8 count seats_count Licensed seats allocated to a tenant.
Projects 9 count projects_count Projects/workspaces under a tenant.
Computational Resources 10 CPU-sec/GB-hr compute_seconds CPU/memory compute consumption (normalise to seconds or GB-hours).
Hours 11 hours hours_used Time-bound use of a feature/service (lab time, agent time).
Data Processing 12 bytes/ops bytes_processed Data processed by pipelines/AI/etc., measured by bytes or operation count.
Concurrent Users 13 count concurrent_users Simultaneous active users allowed.
Features Activated 14 count features_activated Number of modules/features switched on.
Custom Limit 15 varies custom/<key> Extension point for domain-specific limits; define a concrete key & unit.

Meter mapping hints

  • Use one canonical meter key per dimension (snake_case) for counters and billing exports.
  • Prefer the smallest atomic unit (e.g., bytes, seconds) in storage; convert for UI at query-time.
  • Keep dimension→feature linkage via FeatureKey (not navigations) in metering/billing services.

ResetPeriodEnumeration

Defines when counters reset (the “when” of quotas/usage).

Per-member properties shown in the table:

  • Name / ShortName — Display labels as defined in the enum.
  • Value — Stable integer for storage/wire contracts.
  • Cadence (ISO-8601) — Canonical duration for fixed periods (if applicable).
  • Anchor — Calendar / Anniversary / Manual / Rolling / Per-Event.
  • IsRolling — Whether the window is sliding rather than fixed.
  • Notes — Operational guidance.

The following members reflect the current code list. (Semantics below clarify anchor and rolling behavior.)

Name (ShortName) Value Cadence (ISO-8601) Anchor IsRolling Notes
Daily 1 P1D Calendar No Resets every day at configured cutover (typically 00:00 UTC or tenant-local).
Weekly 2 P7D Calendar No Resets once a week on configured weekday (e.g., Monday 00:00).
Monthly 3 P1M Calendar No Resets on 1st of month or billing anchor date.
Quarterly 4 P3M Calendar No Resets at end of quarter (Mar/Jun/Sep/Dec) unless tenant-specified.
Annually 5 P1Y Anniversary No Resets yearly at contract anniversary or configured date.
Hourly 6 PT1H Calendar No Resets on the hour boundary. Useful for burst control.
Custom 7 varies Manual No Tenant/model-defined cadence; document in policy.
On-Demand 8 n/a Manual No Reset performed by admin action/API; audit required.
Bi-Weekly 9 P14D Calendar No Resets every 14 days; align with payroll-style schedules if needed.
Bi-Monthly 10 P2M Calendar No Resets every two months; ensure end-of-month handling for short months.
Per-Transaction 11 n/a Per-Event No Resets after each usage event; behaves like “no accumulation.”
Rolling 24 Hours 12 PT24H Rolling Yes Sliding window of the last 24h from “now”; not tied to wall-clock cutovers.

Meter mapping hints

  • For fixed windows (Daily/Monthly/etc.), bucket usage by calendar anchors; for tenant-specific anchors, store the anchor in the subscription.
  • For rolling windows, use timestamp-based sliding counters (ring buffers or decay queues).
  • Per-Transaction is equivalent to enforcing at event time (no carry-over).
  • On-Demand resets must be audited and should emit a quota.reset_requested|applied event in Metering.

Cross-cutting enumerations: Commerce & Lifecycle

BillingCycleEnumeration

Represents the recurrence at which charges are applied or invoices are generated.

Per-member properties shown below:

  • Name / ShortName — Display label (from code); recommended WireKey (slug) for tokens/exports.
  • Value — Stable integer for storage/wire contracts.
  • Cadence (ISO-8601) — Canonical interval for fixed cycles.
  • Anchor — Calendar/Anniversary/Manual.
  • Description — Billing & proration semantics.
Name (ShortName) WireKey Value Cadence (ISO-8601) Anchor Description
Monthly monthly 1 P1M Calendar Charges every month; proration anchored to the subscription start or tenant billing anchor.
Quarterly quarterly 2 P3M Calendar Charges every 3 months; align with fiscal quarters when configured.
Semi-Annually semi_annually 3 P6M Calendar Charges every 6 months; same proration rules as Monthly/Quarterly.
Annually annually 4 P1Y Anniversary Yearly charges; common for enterprise contracts and prepaid terms.
One-Time one_time 5 n/a Manual Single, non-recurring charge (setup, migration). No proration.
Custom custom 6 varies Manual Contract-specific cadence; include a required metadata field with the computed schedule.

Notes

  • Proration is applied for mid-period seat/plan changes for all fixed cadences; never for One-Time.
  • Custom cycles must record the next run date to drive invoicing and reminders.

PricingTypeEnumeration

Describes the economic model applied by a pricing/rating engine.

Per-member properties shown below:

  • Name / ShortName — Display label and WireKey (slug).
  • Value — Stable integer for storage/wire contracts.
  • Category — High-level bucket used in UIs and analytics.
  • Description — When/why to use; how it combines with billing/usage.
Name (ShortName) WireKey Value Category Description
Fixed Price fixed_price 1 Flat Single recurring price independent of seats/usage.
Tiered Pricing tiered_pricing 2 Usage Unit price changes by usage tiers (e.g., 0–1M, 1–10M).
Volume-Based Pricing volume_pricing 3 Usage Price per unit depends on total volume purchased/committed.
Usage-Based Pricing usage_based 4 Usage Pay strictly for what is consumed (e.g., tokens, API calls).
Per User Pricing per_user 5 Seat Base × seats; may include minimum seat policy.
Per Feature Pricing per_feature 6 Add-on Price varies by selected modules/add-ons.
Freemium freemium 7 Promo Free tier with feature/usage caps; upsell to paid tiers.
Subscription-Based subscription 8 Flat Recurring subscription (often combined with seats/usage).
Pay-As-You-Go payg 9 Usage Pure consumption billing with no commitment.
One-Time Purchase one_time_purchase 10 One-off Lifetime or timeboxed license; pairs with One-Time cycle.

Notes

  • Many real plans are hybrids (e.g., subscription base fee + tiered usage). Capture with a PricingModel + BillingRule composition.

ProductStatusEnumeration (extended lifecycle)

Represents the publication lifecycle of a Product. We extend the current set to match DDD lifecycle semantics while preserving backward compatibility.

Per-member properties shown below:

  • Name / ShortName — Display label and WireKey (slug).
  • Value — Stable integer for storage/wire contracts.
  • IsTerminal — Whether the state forbids further changes.
  • Description — Business meaning and mutability rules.
Name (ShortName) WireKey Value IsTerminal Description & mutability rules
Draft draft 0 Work-in-progress. Keys editable; Editions/Features may be attached/removed freely. Not visible to customers.
Published published 1 Publicly available. Keys lock; only additive changes allowed (new Editions/metadata).
Deprecated deprecated 2 Sunsetting. No new tenants; existing tenants supported. Only security/compat fixes allowed.
Retired retired 3 End-of-life. Hidden from catalogs; migrations complete. No further changes except archival/exports.

Compatibility mapping (for existing code)

  • ActivePublished; InactiveDraft (or Deprecated based on usage); DeprecatedDeprecated. Document the mapping in code comments and migration scripts.

EditionStatusEnumeration (extended lifecycle)

Same semantics as Product, but at the Edition level.

Name (ShortName) WireKey Value IsTerminal Description & mutability rules
Draft draft 0 Composition editable (add/remove features, quotas). Not selectable by tenants.
Published published 1 Selectable by tenants. Key locks; features must all be Published. Only additive changes permitted.
Deprecated deprecated 2 Hidden for new sales; existing tenants remain. Only critical fixes.
Retired retired 3 Not assignable; migrations completed; immutable except archival.

Compatibility mapping (for existing code)

  • ActivePublished; InactiveDraft (or Deprecated); DeprecatedDeprecated.

FeatureStatusEnumeration (new)

Completes the lifecycle symmetry for Feature entities.

Name (ShortName) WireKey Value IsTerminal Description & mutability rules
Draft draft 0 Spec under development; key editable; default quotas/rules can be defined.
Published published 1 Available for inclusion in editions. FeatureKey locks; only additive metadata allowed.
Deprecated deprecated 2 To be removed; cannot be added to new editions; existing editions may keep it until migration.
Retired retired 3 Removed from catalogs; cannot be referenced by active editions; historical events remain.

Lifecycle semantics & mutability (all three: Product, Edition, Feature)

  • Key immutability: Key/FeatureKey becomes immutable at Published.
  • Additive-first: After Published, you may add metadata/links (e.g., new tags, optional descriptors) but not remove required ones that would break consumers.
  • Deprecation discipline: Enter Deprecated with a sunset timeline, migration guidance, and event emission (*.deprecated).
  • Retirement: Retired entities are immutable except archival and export operations. Remove from discovery UIs; keep in read models for historical queries.
  • Eventing: Transitions emit canonical events (product.published, edition.published, feature.published; *.deprecated; *.retired).
  • Policy checks: Publishing requires all linked entities to be Published and compatibility constraints satisfied (e.g., feature version requirements).

Migration note The extended lifecycle is additive. Keep existing integer values for Active/Inactive/Deprecated where serialized, and introduce mapping functions to the new states in adapters. For new storage, adopt the extended set directly to prevent ambiguity going forward.


Shared value objects (Catalog)

These value objects are small, immutable pieces of the Catalog model that carry meaning and validation. They are owned by aggregates (e.g., Product, Feature, EditionFeature) and persisted inline. You won’t find them in your current interfaces—that’s expected; this section explains why we introduce them and how to use them.

Why VOs here

  • They prevent accidental drift in identifiers/limits that show up in contracts (APIs, events, tokens).
  • They centralize validation (one place to define allowed characters, ranges, and mutability rules).
  • They are EF-friendly as owned types (no identity, embedded columns).

ProductKey (VO)

Purpose Canonical, URL-safe identifier for a Product. Used in URLs, events, and cross-service references. Immutable after the Product is first Published.

Properties (with XML-style intent)

Property Type Description
Value string Canonical slug for the product. Lowercase; a–z, 0–9, -, _, . only. Must start/end with alphanumeric. Length 3–64. Immutable after Publish.

Creation & validation

  • Normalize by: trim → lowercase → replace spaces with - → collapse repeated separators → validate against regex ^[a-z0-9](?:[a-z0-9_.-]{1,62})[a-z0-9]$.
  • Uniqueness: globally unique within Catalog (not per tenant).
  • Reserved examples: default, all, system (document your reserved set if any).

Examples

  • crm-suite, analytics.core, config-flags

ORM notes

  • Map as an owned type with a single nvarchar(64) column (e.g., ProductKey).
  • Add a unique index on ProductKey.

Serialization

  • Serialize as a string (not as an object) to keep wire contracts compact.

FeatureKey (VO)

Purpose Canonical identifier for a Feature, referenced by Editions, tokens/entitlements, and downstream services (Config, Billing, Metering). Immutable after the Feature is Published.

Properties

Property Type Description
Value string Canonical feature key. Lowercase slug; same character set as ProductKey. Global uniqueness across Catalog. Immutable after Publish.

Creation & validation

  • Same normalization and regex as ProductKey.
  • Global uniqueness: a FeatureKey must be unique across all products/editions, because entitlements and meters key off it.

Examples

  • webhooks, feature-flags_advanced, ai.tokens

ORM

  • Owned type; store as nvarchar(64) column (e.g., FeatureKey).
  • Index for fast lookup and join to link entities (e.g., EditionFeature).

Serialization

  • As string in APIs/events/claims.

Quota (VO)

Purpose Declarative ceiling for a usage dimension. In Catalog, Quotas are advisory (describe editions/features); Metering enforces them at runtime.

Properties

Property Type Description
Limit decimal? Maximum allowed units for the period. null = Unlimited; 0 = Disabled (no usage). Non-negative. Supports fractional units (e.g., hours).
LimitType LimitTypeEnumeration Dimension key (e.g., API Calls, Storage). Aligns with metering/billing dimension catalogs.
ResetPeriod ResetPeriodEnumeration When counters reset (e.g., Monthly, Rolling 24 Hours). Required when Limit is not null.

Semantics & invariants

  • If Limit is specified → ResetPeriod must be specified.
  • Limit = null → unlimited; still publish the LimitType for clarity.
  • Limit = 0 → blocked/disabled; used for Freemium feature placeholders.
  • The unit is implied by LimitType (bytes, count, hours…); UI conversions (e.g., GB) happen at read time.

Examples

  • { Limit: 1000000, LimitType: API Calls, ResetPeriod: Monthly }
  • { Limit: 10, LimitType: Projects, ResetPeriod: On-Demand }
  • { Limit: null, LimitType: Storage, ResetPeriod: Monthly } // Unlimited storage

ORM

  • Owned type embedded in parent (e.g., Feature.DefaultQuota, EditionFeature.QuotaOverride).
  • Columns: Quota_Limit (decimal(18,4)), Quota_LimitType (int), Quota_ResetPeriod (int).
  • Index Quota_LimitType if you query by dimension often.

Serialization

  • As an object with three fields; omit Limit when null to indicate Unlimited.

KeyValueTag (VO)

Purpose Lightweight metadata for discovery, search, and telemetry. Never used to drive business invariants or authorization.

Properties

Property Type Description
Key string Tag name, 1–64 chars. Lowercase recommended; a–z, 0–9, -, _, .. No PII.
Value string Tag value, up to 256 chars (or your chosen limit). May be free text; avoid secrets and PII.

Guidelines

  • No business logic: tags don’t alter entitlements or pricing.
  • Reserved prefixes: consider sys: for internal tags; reject unapproved internal prefixes at write time.
  • Duplicates: disallow duplicate keys within the same owner; last write wins if you must allow it.

Examples

  • { Key: "category", Value: "analytics" }
  • { Key: "owner_team", Value: "growth" }
  • { Key: "region_hint", Value: "eu" }

ORM

  • Either: (a) owned collection (separate table *_Tags with composite key {OwnerId, Key}), or (b) JSON column for low-cardinality tags. Choose based on query needs.

Serialization

  • As array of { key, value } objects; order not significant.

How these VOs change your current interfaces (non-breaking adoption)

  • Keep Product.Key : string and Feature.Key : string in interfaces, but document that they represent ProductKey.Value / FeatureKey.Value.
  • In concrete aggregates, wrap the string in the VO and apply validation at creation and on publish transitions.
  • For Quota, start by using it where you already carry limits: default limits on Feature, overrides on EditionFeature.
  • Tags are optional; if you already carry free-form metadata, map it to KeyValueTag[] for consistency.

Event impact

  • Always include key (string) for products/features in events (not the whole VO).
  • For quotas, include { limit, limitType, resetPeriod } fields directly in event payloads; omit limit when unlimited.

Ownership

  • These VOs belong to the Catalog context. Other contexts consume their scalar values (e.g., FeatureKey strings, quota fields) via events/read models—no cross-context navigations.

Product (Aggregate Root, Core Metadata)

Relationships

  • HasMany → IEdition A Product owns its lineup of Editions. The Edition model carries a Reference → IProduct back to its owner, and the Product model exposes a collection of editions.
  • HasMany → IBusinessModel Product is also included in zero or more Business Models; the Product model exposes a collection IncludedInBusinessModels for that purpose.

Properties (interface contract)

The table lists each property on IProduct (as implemented today) with DDD semantics. When a VO is referenced (e.g., ProductKey), the interface surface remains string for compatibility; validation/mutability rules come from the VO spec.

Property Type Description & rules
ProductId Guid Aggregate identifier for storage and events; opaque and stable.
Name string Internal product name (engineering/ops). Free text; 1–200 chars. Not necessarily unique globally.
DisplayName string Human-friendly label shown in portals and marketing UIs. 1–200 chars; localized copies may exist in read models.
Key string Canonical ProductKey slug. Lowercase [a–z0–9._-], 3–64 chars, unique within Catalog. Immutable after Published. Used in URLs, events and cross-service references.
Description string Narrative description of the offering and scope. May include links to docs/SLAs (store links, not large HTML).
ProductCategory string High-level grouping (e.g., CRM, ERP, Analytics) for discovery and reporting; does not affect entitlements.
Status ProductStatusEnumeration Lifecycle state (Draft/Published/Deprecated/Retired). Keys lock at Published; only additive changes allowed thereafter.
CreationDate DateTime UTC timestamp when the product was created. Used for audit and ordering.
LatestVersion string? Optional catalog version label (e.g., 1.4). Bumped on significant publish events; semantic only.
ReleaseNotes string? Optional human-written summary of changes included in the latest publish.
DeactivationDate DateTime? Optional sunset anchor for deprecating/retiring the product; read models use it to hide from sales/UX after the date.
Editions IList<IEdition> HasMany collection owned by Product. Each Edition composes features and pricing independently. Loaded explicitly in commands that need lineup invariants.
IncludedInBusinessModels IList<IBusinessModel> HasMany collection of Business Models that include this product (organizational/commercial grouping). Often read-only from Product’s perspective.

These properties are not in the current interface but are part of the blueprint semantics. Add them if/when you extend the contract.

Property Type Why it helps
PublishedAtUtc DateTime? Trace the first time this Product reached Published; simplifies reporting and time-based policies.
DeprecatedAtUtc DateTime? Start of deprecation window; downstream services can warn customers consistently.
RetiredAtUtc DateTime? Actual EOL; used by cleanup/export jobs.
Tags IReadOnlyList<KeyValueTag> Non-authoritative metadata for search/telemetry (owner team, category hints).
DefaultEditionId Guid? Hint for provisioning flows to pick an edition when not specified explicitly (must refer to a Published edition).

Invariants (type-level)

  • A Product cannot transition to Published unless it has at least one Published Edition in Editions.
  • Key is immutable once the Product reaches Published.
  • When Status = Deprecated, new tenants must not be assigned to any edition of this product via self-serve; enterprise assignments may require an override + audit.
  • When Status = Retired, the product must be hidden from catalogs; only archival/export operations are allowed.

Domain events

  • product.published — Emitted when transitioning to Published; includes productId, key, publishedVersion, and minimal snapshot for catalogs.
  • product.deprecated — Emitted when entering Deprecated; include optional sunsetDate and migration guidance ref.
  • product.retired — Emitted at EOL; consumers should stop offering this product and preserve only historical records.

(Event envelopes follow the standard: eventId, occurredAtUtc, aggregateId, aggregateVersion, optional tenantId, and schemaVersion.)


ORM notes

  • Editions navigation: map as HasMany with FK ProductId on Edition. Load explicitly for commands that check publish invariants; avoid global lazy loading to prevent N+1.
  • Cascade rules: cascade only for owned value objects (e.g., tags if modeled as an owned collection). Do not cascade deletes across aggregate boundaries; use lifecycle states (Deprecated/Retired) instead.
  • Indexes: unique index on Key; nonclustered index on Status and (Status,DeactivationDate) for catalog queries.
  • Concurrency: add an optimistic concurrency token (e.g., rowversion) to the concrete class; not part of the interface.

Notes for command handlers

  • Create: validate Key format (per ProductKey VO) and ensure uniqueness.
  • Publish: ensure at least one edition is Published; emit product.published.
  • Deprecate/Retire: set deprecation/retirement timestamps and emit events; block new sales paths accordingly.

This anchors Product as the true aggregate root of the Catalog and sets the rules that upcoming sections (Edition, Feature, EditionFeature, SLA, Pricing/Business Models) build on.


Edition (Catalog)

Relationships

  • Reference → IProduct (parent) Each Edition belongs to exactly one Product; the Edition interface exposes a navigation to its owning IProduct.
  • HasManyThrough → IEditionFeature (Edition↔Feature link) An Edition’s feature set is composed via the explicit link entity IEditionFeature (no implicit M:N). This allows behavior like effective dating, overrides, and custom rules per pairing.
  • HasMany → IServiceLevelAgreement (attached SLAs) Editions may attach one or more SLA packages; the interface exposes a collection of IServiceLevelAgreement. (In practice this is a many-to-many relationship modeled via a join table.)
  • Reference/HasOne → IEditionPricingModel (current shape) Current interface models an Edition-scoped pricing object (EditionPricing). See pricing/effective-dating guidance below.

Properties (interface contract)

Property Type Description & rules
EditionId Guid Stable identifier for storage/events. Opaque and immutable.
Name string Internal name used by engineering/ops. 1–200 chars; not required to be unique globally.
DisplayName string Human-friendly label shown in catalogs/UIs (e.g., “Enterprise”). May have localized copies in read models.
Key string Edition key/slug, unique within its Product; lowercase [a–z0–9._-], 3–64 chars. Immutable after Publish.
Description string Narrative of scope, included capabilities, and limits. Keep links/refs; avoid large HTML payloads.
CreationDate DateTime UTC timestamp when the Edition was created. Used for audit and ordering.
Status EditionStatusEnumeration Lifecycle (Draft/Published/Deprecated/Retired). Keys lock at Published; only additive changes allowed thereafter.
Product IProduct Reference to the owning Product (same bounded context). Commands that change composition should load this for cross-aggregate checks only when required.
EditionPricing IEditionPricingModel HasOne Edition-specific pricing (current shape). See invariants on effective windows and price bounds.
EditionFeatures IList<IEditionFeature> HasManyThrough link entities defining composition (feature, effective/expiry, overrides). No implicit M:N.
ServiceLevelAgreements IList<IServiceLevelAgreement> HasMany (implemented as M:N) SLA packages attached to this Edition. Typically exactly one active at a time.

Property Type Why it helps
Version string SemVer for Edition composition; increments on publish with material changes. Useful for cache keys & consumer diffs.
PublishedAtUtc DateTime? First time this Edition reached Published; helps entitlement stamping and rollout analytics.
DeprecatedAtUtc DateTime? Start of deprecation window; downstream can warn/upsell/migrate.
RetiredAtUtc DateTime? EOL marker; used by cleanup/export processes.
Tags IReadOnlyList<KeyValueTag> Non-authoritative metadata for discovery/telemetry (e.g., “vertical: healthcare”).

Invariants (type-level)

  • Publish readiness

  • All EditionFeatures.Feature must be Published when the Edition transitions to Published.

  • Product.Status must be Published or change atomically to Published with this Edition (policy choice; prefer product published first).
  • Key immutability

  • Key becomes immutable at Published.

  • Composition integrity

  • (EditionId, FeatureId) pairs are unique per effective window; no overlapping active windows for the same pair.

  • If IEditionFeature.QuotaOverride (when modeled) or MaxUsageLimit is set, it must be ≥ 0 and align with the dimension semantics; ExpiryDateEffectiveDate.
  • Pricing integrity

  • EditionPricing windows must not overlap for the same Edition/PricingModel; enforce MinimumPrice ≤ BasePrice ≤ MaximumPrice.

  • SLA attachment

  • At most one active SLA per Edition at a time; new versions create new rows, old ones become inactive with ExpiryDate.


Domain events

  • edition.published — emitted on transition to Published; payload includes editionId, productId, key, compositionVersion, minimal snapshot of features (keys) and SLAs.
  • edition.changed — emitted when composition/pricing/SLAs materially change (e.g., feature added/removed, pricing window updated). Include diffs or new compositionVersion.
  • edition.deprecated — emitted on entering Deprecated; include sunsetDate and recommended migration path.

(Events follow the platform envelope: eventId, occurredAtUtc, aggregateId, aggregateVersion, optional tenantId, schemaVersion.)


ORM notes

  • Product reference

  • FK: ProductId on Edition; map HasOne(e => e.Product).WithMany(p => p.Editions); require FK. (Same bounded context.)

  • EditionFeatures (explicit join)

  • Map IEditionFeature with composite key (EditionId, FeatureId, EffectiveDate) to support time-varied composition; unique filtered index to prevent overlapping windows.

  • Navigations: Edition.HasMany(e => e.EditionFeatures); EditionFeature.HasOne(x => x.Edition) and .HasOne(x => x.Feature).
  • SLAs (M:N via join table)

  • Join: EditionSla(EditionId, ServiceLevelAgreementId, EffectiveDate, ExpiryDate?); enforce one active SLA per edition.

  • EditionPricing (HasOne)

  • Edition.HasOne(e => e.EditionPricing).WithOne(p => p.Edition) (if modeled 1:1 active); or HasMany when supporting historical windows. Enforce price bounds and non-overlap.

  • Indexes

  • Unique index on (ProductId, Key); nonclustered on Status, and on EditionId in the join tables for fast lookups.

  • Loading

  • Command handlers: load minimal graph needed for invariants (e.g., EditionFeatures when publishing).

  • Reads: project to DTOs (EditionView, EntitlementsView) instead of returning deep graphs.

This section fixes the Edition’s role as the composition anchor: it references its Product, composes Features through EditionFeature, binds an active SLA, and (in the current shape) carries an Edition-scoped pricing object for commercial behavior.


Feature (Catalog) & default limits

Relationships

  • HasMany → IBillingRule A Feature can define one or more billing rules that describe how to charge for that capability (overage, thresholds, effective dating).
  • HasManyThrough → IEditionFeature (to Editions) Editions include Features via the explicit link entity IEditionFeature (no implicit many-to-many). This enables effective/expiry windows and per-edition overrides.
  • HasMany → IAccessRule Feature-scoped access rules (and their exceptions) describe who can do what at this feature scope.
  • HasMany → IUsageLimit Feature-scoped default limits (dimension + cap + reset period). Editions can override through IEditionFeature.

Properties (interface contract)

Property Type Description & rules
FeatureId Guid Aggregate identifier; opaque and stable for storage/events.
Name string Internal feature name for engineering/ops; 1–200 chars.
DisplayName string Human-friendly label for portals and docs; localized variants live in read models.
Key string Canonical FeatureKey slug. Lowercase [a–z0–9._-], 3–64 chars. Globally unique in Catalog. Immutable after Published. Used by entitlements, metering, config, and billing.
Description string Narrative description and scope; keep links rather than large HTML.
CreationDate DateTime UTC timestamp when created.
AccessRules IList<IAccessRule> Feature-scoped RBAC/ABAC rules; may carry AccessType, AccessScope, Condition, and effective windows.
UsageLimits IList<IUsageLimit> Default per-feature limits (dimension, limit value, reset period). Editions may override.
BillingRules IList<IBillingRule> Pricing/rating rules (cycle, thresholds, min/max, discount, effective/expiry).

Property Type Why it helps
Status FeatureStatusEnumeration Aligns lifecycle with Product/Edition (Draft/Published/Deprecated/Retired) and locks Key at Published.
DefaultQuota Quota? (VO) Canonical single “most important” limit for this feature (dimension + cap + reset); mirrors entries in UsageLimits and simplifies entitlement stamping.
PublishedAtUtc / DeprecatedAtUtc / RetiredAtUtc DateTime? Clear lifecycle anchors for rollout analytics, sunset comms, and archival.
Tags IReadOnlyList<KeyValueTag> Non-authoritative metadata (search/telemetry).

Note: these are additive to your current IFeature contract and do not break existing callers. Validation and immutability are enforced in the concrete aggregate, not via the interface.


Invariants

  • Key uniqueness & immutability Key must be globally unique across all features; once Published, it cannot change. (Tokens, meters, and flags depend on stability.)
  • Default quota validity If DefaultQuota is present (or any UsageLimits entry), it must specify a valid LimitType and ResetPeriod. For finite limits, value must be ≥ 0; null means Unlimited.
  • Rule consistency Billing rules must satisfy MinimumPrice ≤ OverageRate×threshold ≤ MaximumPrice where meaningful; effective windows must not overlap for the same rule.
  • Access rule scope Feature-scoped access rules must use AccessScope=Feature (or a narrower scope) and valid AccessType; exception rules must reference their parent rule and share compatible scope/time windows.

Domain events

  • feature.published — on transition to Published; include featureId, key, and optional defaultQuota snapshot.
  • feature.deprecated — on entering sunsetting; include sunsetDate and migration guidance reference.
  • feature.retired — at EOL; hides feature from catalogs; leaves historical reads intact.

(Event envelope per platform standard: eventId, occurredAtUtc, aggregateId, aggregateVersion, optional tenantId, schemaVersion.)


ORM notes

  • EditionFeatures inverse Map EditionFeature as an explicit join entity: composite key such as (EditionId, FeatureId, EffectiveDate) to support effective-dating; ensure uniqueness per active window. Navigations from Feature to editions are via EditionFeature only.
  • Rules collections AccessRules, UsageLimits, and BillingRules are separate tables with FKs back to Feature. Avoid cascading deletes across aggregates; prefer lifecycle statuses on rules.
  • Indexes Unique index on Key; nonclustered indexes on (FeatureId, EffectiveDate) in rules and joins for quick lookups by time window.

Guidance for use with Editions

  • Use IEditionFeature to include a Feature in an Edition and optionally override limits or attach edition-specific rules (custom access/usage/billing).
  • When an Edition is Published, every included Feature must already be Published, and any overrides must be compatible with the feature’s dimensions and reset semantics.

EditionFeature (link/entity with behavior)

Relationships

  • Reference → IEdition Connects the link to its owning edition (same bounded context).
  • Reference → IFeature Connects the link to the included feature (same bounded context).

Properties (interface contract + DDD semantics)

Property Type Description & rules
EditionFeatureId Guid Surrogate identifier for the link entity (useful for event correlation and auditing).
CreationDate DateTime When this link row was created (UTC).
Edition IEdition Reference to owning Edition. The FK (EditionId) is persisted; navigation is same-context only.
Feature IFeature Reference to Feature being included. Persist FeatureId as FK.
EffectiveDate DateTime Start of validity (inclusive). Required.
ExpiryDate DateTime? End of validity (exclusive). null = open-ended.
IsIncluded bool Whether the feature is currently part of this edition (on/off switch).
MaxUsageLimit int? Simple cap for this edition-feature pairing (nullable = no cap). For richer caps, use CustomUsageLimits.
CustomAccessRules IList<IAccessRule> Access rules scoped to this edition-feature (override/extend feature defaults).
CustomUsageLimits IList<IUsageLimit> Usage limits scoped to this edition-feature (override feature defaults).
CustomBillingRules IList<IBillingRule> Billing/rating rules scoped to this edition-feature (override feature defaults).

The HLD also documents these attributes (IDs/effective dates, per-edition overrides for access/usage/billing).


Property Type Why it helps
Mode string (Enabled | Conditional | Preview) Clarifies inclusion intent beyond IsIncluded: Enabled = fully on; Conditional = gated by rule/flag; Preview = non-SLA early access. (Surfaces clearly in UIs and entitlements.)
FeatureKey string Denormalized FeatureKey for events/claims; avoids loading Feature to emit downstream contracts while maintaining the navigation for commands.
Constraints IReadOnlyList<string> Human/machine-readable compatibility or gating expressions (e.g., “requires ai.tokens ≥ 1M/month”). Helps validate composition pre-publish.
QuotaOverride Quota? (VO) Rich cap model (dimension + limit + reset) when MaxUsageLimit is insufficient; aligns with Catalog VO semantics.
CustomAccessRuleIds / CustomUsageLimitIds / CustomBillingRuleIds IReadOnlyList<Guid> Optional ID-only mirrors for projection/exports when navigations aren’t needed (keeps events/DTOs light).

The HLD calls out per-edition overrides for limits/billing/access; these extensions make the intent explicit and wire-friendly.


Invariants

  • Publish dependency When an Edition transitions to Published, every linked Feature must already be Published. (Edition cannot publish with Draft/Deprecated features.)
  • Uniqueness A given (EditionId, FeatureId) pair may not have overlapping active windows. Enforce either:

  • single active row per pair (IsIncluded=true) at any time; or

  • composite uniqueness on (EditionId, FeatureId, EffectiveDate) plus a filtered index to prevent overlaps.
  • Override compatibility QuotaOverride (or CustomUsageLimits) must use valid LimitType/ResetPeriod combinations; finite limits are ≥ 0.
  • Rule scope CustomAccessRules must target AccessScope = Feature (or narrower) and align with edition/feature IDs. Exceptions must reference their parent rule and share compatible windows.

Event implications (composition changes)

  • Adding/removing or changing an EditionFeature should raise edition.changed with a minimal diff (feature keys added/removed, overrides changed) so consumers (Identity, Config, Billing) can update entitlements and caches. (Edition emits; Feature remains unchanged.)

ORM notes

  • Keys & indexes

  • Keep EditionFeatureId as the surrogate PK (current interface). Add an alternate unique key on (EditionId, FeatureId, EffectiveDate) and a filtered unique to ensure no overlapping active windows.

  • Joins

  • Map explicit many-to-many: Edition (1..*) — EditionFeature (*..1) Feature. No implicit M:N; the link carries behavior (rules, limits, dates).

  • Denormalized keys

  • Persist FeatureKey (string) alongside FeatureId for fast, event-friendly reads; keep it in sync via domain events or write-model guard.

  • Loading strategy

  • Commands that publish or alter composition should load only the EditionFeatures needed for the invariant checks (current and overlapping windows). Reads should project to entitlement DTOs (feature keys + overrides) rather than returning navigations.


Summary EditionFeature is the behavioral join entity that makes Editions composable and expressive: it binds a Feature to an Edition over time, provides per-edition overrides (access, usage, billing), and is the source of truth for downstream entitlements. Its invariants protect publish quality, while explicit ORM mapping keeps the model DDD-pure and query-friendly.


ServiceLevelAgreement (Catalog)

Relationships

  • HasMany ← IEdition (editions reference SLAs) Editions attach SLA packages for contractual commitments; this is a many-to-many relationship realized via a join table (one Edition should have at most one active SLA at a time). The current interface exposes Editions : IList<IEdition>.
  • Reference ← IBusinessModel (organizational linkage) The current interface includes a BusinessModel navigation indicating the business model under which the SLA is defined.

Properties (interface contract + DDD semantics)

The table lists properties present today and DDD-aligned additions. For existing members, names map 1:1 to your interface. For additions, we note “(new)” and intended semantics.

Property Type Description & rules
ServiceLevelAgreementId Guid Stable identifier for storage/events. Consider aliasing as SlaId in DTOs for brevity.
Name string Internal name for ops/engineering; 1–200 chars.
Key string Canonical key/slug (lowercase [a–z0–9._-], 3–64). Used in catalogs/URLs. Immutable once published.
DisplayName string Human-friendly label (e.g., “Enterprise SLA”). Localized copies live in read models.
Description string Narrative of scope, targets, and exclusions. Prefer references to detailed docs over long HTML.
CreationDate DateTime UTC timestamp when the SLA record was created.
ServiceAvailability decimal? Target availability percentage (e.g., 99.9 = 99.9%). null when availability isn’t a stated metric.
ResponseTime TimeSpan? Max initial response time for incidents/requests (e.g., PT1H). Optional when using severity matrices.
ResolutionTime TimeSpan? Max resolution/restoration time for in-scope incidents (e.g., PT4H). Optional when using severity matrices.
PenaltyClauses string? Text describing credits/penalties if targets are missed. Consider renaming to CreditsPolicy for clarity.
EffectiveDate DateTime Start date/time when this SLA version becomes applicable (inclusive).
ExpiryDate DateTime? End date/time when this SLA version ceases to apply (exclusive). null = open-ended.
IsActive bool Convenience flag for current applicability. For effective-dated models this is derived; keep in sync with dates.
BusinessModel IBusinessModel Organizational linkage; use for packaging/quoting. (Same bounded context.)
Editions IList<IEdition> Editions that reference this SLA (many-to-many). Typically exactly one active per Edition.

Property Type Why it helps
Version (new) string SemVer for SLA content; increments on material changes. Useful for diffs and cache keys.
Status (new) SlaStatus (Draft/Published/Deprecated/Retired) Lifecycle control consistent with Product/Edition/Feature; locks Key at Published.
SupportWindow (new) string or VO (e.g., SupportWindowSpec) Human/structured availability of support (e.g., 24x7, 9x5 Mon–Fri, with timezone).
ResponseTimes (new) IDictionary<string, TimeSpan> Severity-based targets (e.g., P1: PT1H, P2: PT4H), superseding single Response/Resolution when needed.
CreditsPolicy (new) string Clear, positive naming for penalties/credits (maps from PenaltyClauses).
PublishedAtUtc / DeprecatedAtUtc / RetiredAtUtc (new) DateTime? Lifecycle anchors for comms and archival.

These are documentation-level additions; introduce incrementally without breaking existing contracts by keeping current fields and adding new ones.


Invariants

  • One active SLA per Edition For any Edition at a given time, there must be at most one SLA whose effective window covers “now”. Enforce through a filtered unique index on the Edition-SLA join (EditionId) where (EffectiveDate ≤ now < ExpiryDate || ExpiryDate IS NULL).
  • Immutability of published versions Once an SLA version is Published, its content is immutable. Changes require a new Version with a new effective window (the prior version gets an ExpiryDate).
  • Temporal coherence EffectiveDate must be ≤ ExpiryDate (when present). Windows for the same SLA Key should not overlap—prefer versioned rows.
  • Lifecycle rules Transitions to Deprecated should include a sunset policy (how/when Editions move to a replacement). Retired SLAs must not be attachable to Editions.

ORM notes

  • Join mapping (Edition↔SLA) Model an explicit join entity/table, e.g., EditionSla(EditionId, SlaId, EffectiveDate, ExpiryDate?). Add a filtered unique index to enforce “one active SLA per Edition”.
  • Owned VOs (optional) If you adopt SupportWindowSpec or structured ResponseTimes, map them as owned types (OwnsOne/OwnsMany).
  • Lifecycle vs delete Prefer status transitions (Deprecated, Retired) over hard delete. If you need soft delete, ensure joins ignore deleted rows and events are emitted on status change.
  • Indexes

  • Unique index on Key.

  • Nonclustered on (IsActive, EffectiveDate) for quick queries.
  • For versioning, an index on (Key, EffectiveDate DESC) helps “current version” lookups.

Notes for command handlers

  • Publish: validate key uniqueness, set PublishedAtUtc, and ensure no overlapping version window for the same Key.
  • Attach to Edition: enforce “one active SLA per Edition” at write time; if switching, set the current one’s ExpiryDate before attaching the new one.
  • Deprecate/Retire: update lifecycle timestamps and emit catalog events (if you introduce them) so downstream UIs and contracts update accordingly.

This frames SLAs as stable, versioned commitments that Editions attach at a point in time, with clear lifecycle and effective-dating rules—fully aligned with the rest of the Catalog model.


BusinessModel (Catalog)

Relationships

  • HasMany → IProduct, IEdition, IFeature, IServiceLevelAgreement, IPricingModel A Business Model groups catalog objects for commercial packaging and go-to-market. The interface exposes collections for Products, Editions, Features, SLAs, and Pricing Models.
  • Reference → IPricingModel (Default) One default pricing model defines the baseline commercial approach for this Business Model.

Properties (interface contract)

Property Type Description & rules
BusinessModelId Guid Stable identifier for storage/events; opaque and immutable.
Name string Internal name for ops/engineering; 1–200 chars.
Key string Canonical slug (lowercase [a–z0–9._-], 3–64). Used in admin UIs/exports. Prefer immutability after publish (see invariants).
DisplayName string Human-friendly label for sales/portal surfaces.
Description string Narrative of the commercial strategy (target segments, packaging notes).
CreationDate DateTime UTC timestamp when the model was created.
DefaultPricingModel IPricingModel Reference to the default pricing model used by this Business Model. Must also appear in PricingModels.
Products IList<IProduct> HasMany collection of packaged products. Use for bundles or portfolios.
Editions IList<IEdition> HasMany collection of editions included/offered under this model.
Features IList<IFeature> HasMany collection of features included as add-ons or à la carte items.
ServiceLevelAgreements IList<IServiceLevelAgreement> HasMany collection of SLAs this model can attach to offerings.
PricingModels IList<IPricingModel> HasMany available pricing models (tiers/variants) under this business model.

Property Type Why it helps
Status BusinessModelStatus (Draft/Published/Deprecated/Retired) Align lifecycle with Catalog entities; lock Key at Published.
Version string SemVer for packaging contents; bump when membership/pricing defaults materially change.
PublishedAtUtc / DeprecatedAtUtc / RetiredAtUtc DateTime? Lifecycle anchors for sales enablement and archival.
Tags IReadOnlyList<KeyValueTag> Non-authoritative metadata (e.g., “segment: smb”, “geo: eu”).

Invariants

  • Default pricing presence DefaultPricingModel must be an element of PricingModels. (Reject writes otherwise.)
  • Coherent membership Editions must belong to Products that are also included (or the BM declares a policy that allows cross-product editions—document explicitly).
  • Publish discipline To mark a Business Model as Published, all included Products/Ed itions/Features/SLAs should be in Published (or strictly compatible) states; otherwise stay Draft.
  • Key stability Once Status = Published, Key is immutable; future changes are additive (new memberships/pricing) or versioned (Version++).

ORM notes

  • Explicit many-to-many Use link tables for each relationship to avoid implicit M:N and to allow effective-dating later if needed:

  • BusinessModelProduct(BusinessModelId, ProductId)

  • BusinessModelEdition(BusinessModelId, EditionId)
  • BusinessModelFeature(BusinessModelId, FeatureId)
  • BusinessModelSla(BusinessModelId, SlaId)
  • BusinessModelPricingModel(BusinessModelId, PricingModelId)
  • Default pricing Store DefaultPricingModelId FK on the BM table; enforce presence in the join table (BusinessModelPricingModel) with a FK + unique constraint.
  • Indexes Unique index on Key; nonclustered indexes on each join table’s (BusinessModelId) and (RelatedId) for fast membership queries.
  • Loading strategy Commands typically load only DefaultPricingModel + the specific collection being mutated (e.g., Products). Reads project to lightweight membership DTOs.

  • IPricingModel supplies pricing strategy, currency, base/min/max, billing cycle, and pricing type; it also owns the list of edition-specific prices via IEditionPricingModel.
  • When a BM is Published, prefer editions with active IEditionPricingModel windows for quoting; ensure min≤base≤max and non-overlap on effective dates at the pricing layer.

This positions BusinessModel as the organizational/commercial wrapper that cleanly references pricing defaults and collects eligible catalog items, with explicit invariants and ORM mappings that keep the model DDD-pure and evolution-friendly.


PricingModel (current Catalog location) & EditionPricingModel

Relationships

  • HasMany → IEditionPricingModel (PricingModel→Edition prices) A Pricing Model can define per-edition prices via IEditionPricingModel entries (effective-dated).
  • Reference ← IBusinessModel (default/back-ref) IPricingModel is referenced from a Business Model (as default and within its list of available models).
  • Reference ← IEdition (current EditionPricing nav) Each IEditionPricingModel links back to both its IEdition and its IPricingModel.

IPricingModel — properties (interface contract)

Property Type Description & rules
PricingModelId Guid Stable identifier for storage/events.
Name string Descriptive name (e.g., “Standard”, “Premium”). 1–200 chars.
Key string Canonical slug ([a–z0–9._-], 3–64). Prefer immutability after publish.
DisplayName string Human-friendly label for portals/quotes.
Description string Narrative of strategy (e.g., seat+usage hybrid).
CreationDate DateTime UTC creation timestamp.
PricingType PricingTypeEnumeration Fixed / Per-Seat / Tiered / Volume / PAYG, etc. Drives rating logic.
Currency string ISO 4217 code (e.g., USD). Must match edition prices’ currency.
BasePrice decimal Baseline unit or recurring price before discounts.
DiscountPercentage decimal? Optional percentage discount (0–100).
BillingCycle BillingCycleEnumeration Monthly/Quarterly/Annually/etc. Used for subscription cadence.
MinimumPrice decimal Floor amount—final rated price must be ≥ this.
MaximumPrice decimal Cap amount—final rated price must be ≤ this.
EditionPricingModels IList<IEditionPricingModel> Edition-specific prices under this model (effective-dated).
BusinessModel IBusinessModel Back-reference to the owning/associated Business Model.

Recommended extension (optional)

  • PublishedAtUtc / DeprecatedAtUtc (timestamps) — for lifecycle analytics.
  • Tags : IReadOnlyList<KeyValueTag> — metadata (e.g., segment/geo).

IEditionPricingModel — properties (interface contract)

Property Type Description & rules
EditionPricingModelId Guid Stable identifier (useful for audit & event diffs).
CreationDate DateTime UTC creation time.
BasePrice decimal Edition-specific base price for this pricing model.
DiscountPercentage decimal? Optional percentage discount (0–100).
MinimumPrice decimal Floor amount (coherent with model).
MaximumPrice decimal Cap amount.
EffectiveDate DateTime Start of applicability (inclusive).
ExpiryDate DateTime? End of applicability (exclusive); null = open-ended.
IsActive bool Convenience flag; should reflect now ∈ [EffectiveDate, ExpiryDate).
Edition IEdition Reference to the Edition being priced.
PricingModel IPricingModel Reference to the Pricing Model defining rules/cadence/currency.

Recommended extension (optional)

  • Currency : string — include explicitly if you anticipate model-local overrides or multi-currency (must match the parent model unless a policy permits exceptions).
  • Notes : string? — justification / promotion code reference.

Invariants

  • Bounds coherence: For both IPricingModel and IEditionPricingModel: MinimumPrice ≤ BasePrice ≤ MaximumPrice. Reject updates that violate.
  • Effective range: IsActive must be true only when “now” is within [EffectiveDate, ExpiryDate); EffectiveDate ≤ ExpiryDate when expiry is present.
  • Currency coherence: EditionPricingModel.Currency (if added) must equal the parent PricingModel.Currency; otherwise, mandate an explicit conversion policy and FX source.
  • Non-overlap per (Edition, PricingModel): For a given (EditionId, PricingModelId), effective windows must not overlap. New windows start on or after the prior ExpiryDate.
  • Business Model consistency: If a Business Model declares a DefaultPricingModel, it must also include that model in its PricingModels list (enforce at BusinessModel).

Domain events (pricing changes)

  • pricing.edition_price.added|updated|expired — emitted when an Edition’s pricing window is created/changed/ended. Payload: editionId, pricingModelId, effectiveDate, expiryDate?, basePrice, min, max, discount?.
  • pricing.model.changed — emitted when a Pricing Model’s global properties change (e.g., Currency, BillingCycle, PricingType).

(Events use the platform envelope: eventId, occurredAtUtc, aggregateId, aggregateVersion, optional tenantId, schemaVersion.)


ORM notes

  • Explicit join Map IEditionPricingModel as a concrete table with PK EditionPricingModelId. Add a unique composite on (EditionId, PricingModelId, EffectiveDate) and a filtered unique to enforce non-overlapping active windows per (EditionId, PricingModelId).
  • FKs & indexes FKs to Edition and PricingModel. Nonclustered indexes on (EditionId), (PricingModelId), and (IsActive, EffectiveDate DESC) for “current price” queries.
  • Loading strategy

  • Commands that compute quotes load only the current IEditionPricingModel for the targeted (Edition, PricingModel).

  • Reporting can project historical windows to DTOs; avoid returning deep navigations.

Usage guidance

  • Quoting: select the active IEditionPricingModel for a chosen PricingModel and Edition; apply DiscountPercentage then clamp to [MinimumPrice, MaximumPrice].
  • Promotions: model as separate rules (at Billing context) or as additional IEditionPricingModel windows with explicit notes; keep historical windows immutable for audit.
  • Migration path: if you later move pricing under a dedicated Billing context, keep these interface shapes as read models (IDs/keys/currency/amounts/effective windows) and publish events from Billing that populate them.

BillingRule (as modeled today)

Relationships

  • Reference → IFeature Each billing rule targets a specific Feature; charges are computed in the context of that capability (e.g., overage on API calls of that feature).

Properties (interface contract)

Property Type Description & rules
BillingRuleId Guid Stable identifier for storage/events; opaque and immutable.
Name string Internal name for ops/engineering; 1–200 chars.
DisplayName string Human-friendly label for admin UIs/quoting.
Description string Narrative of how the rule applies (e.g., “$0.01 per API call beyond included 1M/month”).
CreationDate DateTime UTC creation timestamp.
BillingCycle BillingCycleEnumeration Frequency at which the rule is evaluated (Monthly/Quarterly/Semi-Annually/Annually/One-Time/Custom). Drives aggregation/proration.
OverageRate decimal Unit price applied to usage beyond the threshold (e.g., 0.01 per call). Non-negative.
MinimumPrice decimal Floor charge enforced by this rule, regardless of usage. Non-negative.
MaximumPrice decimal Cap on charges produced by this rule. Must be ≥ MinimumPrice.
Discount decimal? Optional discount applied by the rule (percentage or absolute—your current doc treats as numeric; specify policy in pricing engine).
UsageThreshold decimal Level at which the rule starts to apply; measured in the feature’s metered dimension (e.g., calls, bytes). Must be ≥ 0.
EffectiveDate DateTime Start of validity (inclusive). Required.
ExpiryDate DateTime? End of validity (exclusive). null = open-ended.
IsActive bool Convenience flag to toggle application without deleting. Should reflect current effective window.
Feature IFeature Reference to the targeted Feature; determines the dimension and entitlement context for rating.

Property Type Why it helps
Currency string Makes the rule self-contained when evaluated outside a PricingModel context; otherwise require engine to take currency from the Edition/PricingModel.
DimensionKey string Explicit mapping to metering dimension (e.g., api_calls, egress_bytes) when a feature has multiple measurable aspects.
Notes string? Reference to promotion code / contract clause justifying the rule.

(These are not required by your current interface; they can be introduced in a v2 interface or a read-model.)


Invariants

  • Price bounds coherence MinimumPrice ≤ MaximumPrice. Any computed charge C from the rule must satisfy C ∈ [MinimumPrice, MaximumPrice].
  • Threshold & rate validity UsageThreshold ≥ 0 and OverageRate ≥ 0. Reject negative values.
  • Effective window If ExpiryDate is present, then EffectiveDate ≤ ExpiryDate. IsActive should be true iff “now” is within [EffectiveDate, ExpiryDate) (or open-ended).
  • Feature state The associated Feature must be Published while the rule is active; do not activate a rule for Draft/Deprecated features (prevents selling unsupported capabilities).
  • Cycle consistency When paired with subscription cycles, the BillingCycle should align with the subscriber’s cadence (or the pricing engine must prorate across cycles).

Examples (rating intent)

  • Overage on API calls: After 1,000,000 calls/month (UsageThreshold), charge OverageRate = 0.01 per call, clamped to [MinimumPrice, MaximumPrice].
  • One-time setup fee: BillingCycle = OneTime, OverageRate = 0, set MinimumPrice = MaximumPrice = 2500.

ORM notes

  • Effective-dated indexing Add a composite index on (FeatureId, IsActive, EffectiveDate DESC) to efficiently resolve the current rule for a feature. Keep historical rows for audit.
  • Non-cascading relationship Map BillingRule.Feature with a required FK to Feature. Do not cascade delete rules when features are retired; prefer deactivation or expiry.
  • Uniqueness & overlap If you allow multiple rules per feature, constrain by type/priority or add a filtered uniqueness to prevent ambiguous overlaps within the same BillingCycle.
  • Serialization Expose IDs and scalar fields in events/DTOs; avoid shipping navigations. Use FeatureKey in read models for downstream systems.

This section reflects the current interface of IBillingRule and clarifies how to keep it effective-dated, bounded, and aligned with feature lifecycles and billing cycles.


Access Rules (if present in your model)

Relationships

  • Reference → Feature or Edition targets A rule may target a Feature and/or an Edition (both are nullable in the current interface, enabling broad→narrow scoping).
  • HasMany → Exceptions from rule A rule can include exception rules that carve out conditions where the primary rule does not apply. Each exception links back to its AssociatedAccessRule.

IAccessRule — properties (interface contract + DDD semantics)

Property Type Description & rules
AccessRuleId Guid Stable identifier for storage/events; opaque and immutable.
Name string Internal name for ops/eng; 1–200 chars.
Key string Canonical slug (policy catalog key). Treat as immutable once referenced externally.
DisplayName string Human-friendly label shown in admin UIs.
Description string Purpose and scope narrative; include examples of when it applies.
CreationDate DateTime UTC creation timestamp.
AccessType AccessTypeEnumeration What action is governed (Read/Write/Execute/Admin/…); bound to evaluation matrix.
Condition string Expression/predicate evaluated at runtime (ABAC). Recommend treating as ConditionExpression (rename in v2) and documenting the expression language (e.g., CEL/OPA-like).
EffectiveDate DateTime Start of applicability (inclusive).
ExpiryDate DateTime? End of applicability (exclusive); null = open-ended.
IsActive bool Operational toggle; should reflect whether “now” falls within the effective window.
AccessScope AccessScopeEnumeration Where the rule applies (Global/System/Feature/Edition/Tenant/User/…); prefer the narrowest viable scope.
Feature IFeature? Optional Reference to a feature target; when set, the rule applies only within this feature.
Edition IEdition? Optional Reference to an edition target; when set, the rule applies only within this edition.
ExceptionRules IList<IAccessExceptionRule> HasMany exceptions that carve out where the main rule should not apply.

IAccessExceptionRule — properties (interface contract + DDD semantics)

Property Type Description & rules
AccessExceptionRuleId Guid Stable identifier for the exception rule.
Name / DisplayName string Internal/UI labels; keep concise and specific.
Description string What scenario this exception covers; include examples.
CreationDate DateTime UTC creation timestamp.
Condition string Predicate under which the exception negates/modifies the parent rule (recommend “ConditionExpression” in v2).
EffectiveDate / ExpiryDate DateTime / DateTime? Validity window; ExpiryDate exclusive, null = open-ended.
IsActive bool Toggle; should align with effective window.
AccessScope AccessScopeEnumeration Scope of the exception; must be same or narrower than parent rule.
AssociatedAccessRule IAccessRule Reference back to the primary rule this exception modifies.

Invariants

  • Scope narrowing An exception’s AccessScope must be the same or narrower than the parent rule’s scope (e.g., parent=Edition → exception may be Feature/User/Resource within that edition).
  • Time coherence Exception windows must be within or equal to the rule’s window; do not allow exceptions to outlive the main rule. (Enforce at write-time.)
  • No cycles Prevent cyclic references among rules/exceptions (A→B→A). Enforce via validation before persisting and by design (exception only references one parent).
  • Target clarity If both Feature and Edition are specified on a rule, the effective target is the intersection (that feature in that edition). If neither is specified, the rule applies at its declared AccessScope only (e.g., Global/System/Tenant).
  • Activation semantics IsActive should be true iff “now” ∈ [EffectiveDate, ExpiryDate) (or open-ended), and the parent aggregate context (e.g., Feature/Edition) is in a state where such rules are allowed (e.g., Published).

Evaluation notes

  • RBAC + ABAC AccessType (what) and AccessScope (where) combine with Condition (when) to yield the decision. Typical predicates: edition tier, tenant residency, user role, feature flag, data classification.
  • Precedence Evaluate exceptions after the parent rule’s predicate; if an exception matches, it overrides the parent outcome (usually to deny or to further restrict). Document your exact precedence (deny-overrides-allow is common).
  • Token enrichment Emit short names (from enumerations) into scopes/claims; services reconstruct policy checks using their published matrices.

ORM notes

  • Separate tables Persist AccessRule and AccessExceptionRule in distinct tables. FK: AccessExceptionRule.AssociatedAccessRuleId → AccessRule.AccessRuleId.
  • Indexes Add composite indexes for typical evaluations:

  • (AccessScope, FeatureId, EditionId, IsActive, EffectiveDate) on rules.

  • (AssociatedAccessRuleId, IsActive, EffectiveDate) on exceptions.
  • Constraints

  • Check: EffectiveDate <= ExpiryDate (when not null).

  • Check: exception EffectiveDate/ExpiryDate ⊆ parent window (enforce in app logic or via trigger).
  • Loading strategy Query only rules in scope plus their active exceptions; evaluate predicates in the service with the request context.

Suggested v2 refinements (non-breaking to current read models)

  • Rename ConditionConditionExpression in both rule and exception interfaces; document the expression grammar and allowed attributes (edition key, feature key, tenantId, user role, residency, etc.).
  • Add Priority : int on rules to manage overlaps deterministically (lower = evaluated earlier).
  • Add Effect : enum { Allow, Deny } if you want explicit allow/deny semantics on the rule itself (today it’s implied by AccessType + policy matrix).

This section captures how Access Rules and their Exceptions shape effective permissions at Feature/Edition (and broader) scopes, grounded in your existing interfaces and enumeration catalogs.


Catalog events & projections

Event contracts (CloudEvents-aligned)

All events follow the canonical envelope (headers) and versioning rules used across the platform. Headers are required; payloads are additive and backwards-compatible.

Common envelope (headers)

Header Description
type Semantic name with version suffix (e.g., product.published.v1).
id Globally unique event id (ULID/GUID).
source Emitting service/context (e.g., catalog-svc).
specversion Always 1.0.
time RFC3339 event time.
traceId W3C correlation ID; ties to request/command trace.
tenantId Authoritative tenant (omit for pure catalog-wide changes); consumers assume tenant-scoped by default.
edition Snapshot of the tenant’s current edition (when tenant-scoped).
schemaVersion Semantic version of data payload (e.g., 1.0.0).
partitionKey Defaults to tenantId for ordered consumption.
key Idempotency key for the business entity/sequence.

Producers use outbox; consumers use inbox + idempotency keys; DLQs are immutable & observable.


product.published.v1

Minimal fact that a Product became available.

data

  • productId : guid — Aggregate id.
  • key : string — Canonical ProductKey.
  • displayName : string — Human label.
  • version : string — Catalog version of the product definition at publish.
  • editions : { editionId, key, status }[]Keys only and lifecycle states for discoverability.

Notes: Emitted only when Product transitions to Published; keys are immutable after this point.


feature.published.v1

A Feature is available for inclusion.

data

  • featureId : guid
  • key : string — Canonical FeatureKey (globally unique).
  • displayName : string
  • defaultQuota? : { limit, limitType, resetPeriod } — Snapshot for downstream caches.

Notes: Downstream services must not treat quotas in Catalog as enforcement; Metering is the source of truth.


edition.published.v1

An Edition composition is now active.

data

  • editionId : guid
  • productId : guid
  • key : string
  • compositionVersion : string — Incremented on material composition changes.
  • features : { key, mode, quotaOverride? }[]Feature keys with per-edition flags/limits.
  • sla? : { key, version } — Attached SLA pointer.

Notes: Only Published features may appear; event carries a minimal snapshot to build read models.


edition.changed.v1

Edition composition/pricing adjusted post-publish.

data

  • editionId : guid
  • productId : guid
  • compositionVersion : string — New value.
  • diff : { addedFeatures: string[], removedFeatures: string[], changed: { key, from, to }[] }
  • pricing? : { pricingModelId, effectiveDate, expiryDate?, basePrice, min, max, discount? }
  • slaChanged? : { from?: { key, version }, to?: { key, version } }

Notes: Consumers (Config, Identity, Billing) update caches using the diff; pricing windows must be non-overlapping.


entitlements.changed.v1

Effective entitlements for a tenant+edition changed (Catalog → Config → Identity flow).

data

  • tenantId : string — Required; event is tenant-scoped.
  • edition : string — Edition key at time of emission.
  • features : { key, state: "enabled|conditional|preview", quota? }[] — Evaluated pack (post-edition overrides).
  • reason : string — e.g., edition.published, edition.changed, config.override.updated.
  • digest : string — Hash of the entitlement set (for cache-busting).

Notes: Tokens carry snapshots of entitlements; Config is runtime source of truth. This event enables Identity claim enrichment and cache invalidation.


Projections

CatalogView (Product → Edition → Feature)

Denormalized read model to power discovery, quoting UIs, and claim enrichment. Built from product.*, feature.*, and edition.* events.

Field Description
productId, productKey, productDisplayName, productStatus Identification + lifecycle for product.
editions[] Array of: editionId, editionKey, editionDisplayName, status, compositionVersion, publishedAtUtc.
editions[].features[] Array of: featureKey, mode, quotaOverride? { limit, limitType, resetPeriod }.
editions[].sla? key, version of attached SLA (current).
tags? : { key, value }[] Non-authoritative metadata for search.

Indexes: (productKey), (editions.editionKey), and compound on (productStatus, editions.status) for catalog filters. Never exposes navigations—keys and scalars only.


EntitlementsView (Edition → Descriptors)

Tenant-facing projection used to stamp tokens and prime Config caches.

Field Description
tenantId Projection partition key.
editionKey Edition currently assigned.
features[] { key, state, quota? } after applying edition pack + tenant overrides.
digest Stable hash (included in tokens) to detect drift.
updatedAtUtc Last re-computation time.

Sources: edition.published|changed, config.flag.updated|override.updated, subscription/billing events (when edition changes).


Mapping guidance (navigations → DTOs)

  • Catalog to events: Emit IDs and keys, not graphs. For example, Edition publishes feature keys and optional quotaOverride scalars; consumers reconstruct their local models.
  • Events to projections: Consumers build CatalogView/EntitlementsView via idempotent upserts keyed by natural keys (productKey, editionKey, tenantId+digest). Use tolerant readers for additive fields.
  • No rich graphs across contexts: Cross-context DTOs use keys + minimal snapshots; never leak EF navigations or internal invariants over the wire.
  • Security & tenancy: All events carry tenantId where relevant; consumers reject missing/foreign tenant headers and scope processing by partition.

Topic & subscription conventions

  • Topic-per-domain: catalog-events, billing-events, config-events, etc.
  • Subscription-per-consumer with SQL filters on type, tenantId, edition.
  • DLQ per subscription; operator replay via isolation workers w/ circuit breakers.

Versioning & evolution

  • type suffix (.v1, .v2) marks major payload versions; additive fields do not bump major.
  • Deprecations follow a contract governance workflow; producers may dual-publish until consumers confirm adoption.

Example flows

  • Edition updated → entitlements changed: edition.changed.v1 (diff) → EntitlementsView recompute → emit entitlements.changed.v1 → Identity stamps new digest into short-lived tokens; services still consult Config for real-time decisions.

This section standardizes the event-first contracts and the two canonical projections—clear, minimal, and tenant-safe—so downstream services can react, cache, and enforce without ever coupling to Catalog’s internal graphs.


Tenant (Aggregate Root, Tenant Management)

Relationships

  • Reference → IEdition (default seed for provisioning) A Tenant may carry a default Edition reference that seeds configuration/entitlements at activation (optional).
  • HasMany → Contact A Tenant owns its contact book (Owner/Billing/Technical/Support). Contacts live and die with the Tenant.

Types in this context

  • ITenant — aggregate root.
  • ITenantProfile — value object (owned by Tenant).
  • ITenantRegionResidency — value object (owned by Tenant).
  • IContact — child entity.

Interfaces are suggested names; you don’t have these yet—this cycle specifies their contracts for implementation later.


ITenant — properties (contract + DDD semantics)

Property Type Description & rules
TenantId Guid Authoritative identifier for the tenant. Opaque and stable; used as RLS/partition key across services.
LifecycleStatus TenantLifecycleStatus (Provisioning, Active, Suspended, PendingDeletion, Deleted) Current lifecycle stage. Only Active tenants receive service. Deleted is terminal (data may be archived).
Profile ITenantProfile Owned VO. Legal/display identity and commercial/account metadata.
RegionResidency ITenantRegionResidency Owned VO. Data-region policy and physical/logic partition (silo) selection.
Contacts IList<IContact> HasMany child entities with roles (Owner/Billing/Technical/Support/Security). At least one Owner required to activate.
EditionDefaultsRef Guid? Optional default EditionId used to seed feature flags/entitlements at activation (must reference a Published edition).
CreatedAtUtc DateTime Creation timestamp (UTC).
ActivatedAtUtc DateTime? First activation timestamp (UTC). Set when transitioning to Active.
LockedAtUtc DateTime? Residency lock time (UTC) if/when residency becomes immutable by policy.

ITenantProfile — properties (owned VO)

Property Type Description & rules
LegalName string Registered legal entity name (1–256 chars). Required for invoicing/contracts.
DisplayName string End-user display name (1–200 chars). Shown in portals, emails, and headers.
BillingAccountRef string? External billing account/customer id (PSP/ERP/CRM). Do not encode PII beyond the provider’s id.
Domain string? Vanity domain or subdomain used for routing/branding (e.g., acme.example.com). Globally unique if used.
Tags IReadOnlyList<KeyValueTag> Non-authoritative metadata (segment, vertical, owner team). No secrets/PII.

ITenantRegionResidency — properties (owned VO)

Property Type Description & rules
RegionCode string Jurisdiction/region key (US, EU, IL, …). Drives data locality and processor selection.
DataSiloId string Physical/logical partition id (e.g., shard/cluster/tenant db). Used by routers and ODS.
ResidencyLocked bool When true, region/silo are immutable except via regulated migration flow; set on activation if required by policy.

IContact — properties (child entity)

Property Type Description & rules
ContactId Guid Identifier for updates/audit.
Name string Full name (1–200 chars).
Email string RFC-compliant email. Recommended to verify before activation when Role=Owner.
Phone string? E.164 phone (optional).
Role ContactRole (Owner, Billing, Technical, Support, Security) Functional responsibility used for notifications/approvals.

Invariants

  • Residency before activation RegionResidency.RegionCode and DataSiloId must be set (and if policy requires, ResidencyLocked=true) before transitioning to Active.
  • Owner contact Must have ≥ 1 Contact.Role = Owner (with verified email if policy) to activate.
  • Domain uniqueness If Profile.Domain is provided, it must be globally unique across tenants and conform to DNS label rules.
  • Edition defaults If EditionDefaultsRef is set, it must reference a Published Edition. On activation, emit tenant.edition_defaults_applied and seed Config/Identity.
  • Lifecycle transitions

  • Provisioning → Active: only if the above invariants hold.

  • Active → Suspended: service access blocked; data retained.
  • Suspended → Active: allowed after compliance/billing clearance.
  • Active/Suspended → PendingDeletion: requires explicit confirmation/retention checks.
  • PendingDeletion → Deleted: terminal; data archiving/anonymization completed.

Domain events (tenant stream)

  • tenant.created — New tenant record created. Payload: tenantId, profile.displayName, regionResidency.regionCode, timestamps.
  • tenant.activated — Tenant became active. Payload includes editionDefaultsRef? and residency lock info.
  • tenant.suspended / tenant.reinstated — Operational suspension toggled; include reason code.
  • tenant.residency_locked — Residency lock engaged; include region/silo.
  • tenant.deletion_requested — Tenant requested deletion; include earliest deletion date and confirmation channel.
  • tenant.deleted — Terminal; include archival/reference ids.
  • tenant.contact_added|updated — Contact changes; include contact role/email.
  • tenant.edition_defaults_applied — Edition defaults used to seed entitlements/config for this tenant.

All events use standard envelopes (id/time/traceId/schemaVersion), and are partitioned by tenantId.


ORM notes

  • Owned types Map Profile and RegionResidency as owned (OwnsOne) under Tenant. They have no identity and are persisted inline (columns prefixed Profile_*, Residency_*).
  • Contacts Map as a child table with FK TenantId. Add uniqueness constraint on (TenantId, Role, Email) to avoid duplicates. Consider an index on (TenantId, Role).
  • Edition reference Store EditionDefaultsRef as nullable Guid. No navigation across contexts at runtime; read models can join by key when needed.
  • Indexes Unique index on Profile_Domain (when not null). Nonclustered indexes on LifecycleStatus, Residency_RegionCode, and Residency_DataSiloId.
  • Row-level security Use TenantId as the RLS key in shared stores; ensure every multi-tenant table carries it and is enforced in queries.
  • Concurrency Add an optimistic concurrency token (e.g., rowversion) to the concrete Tenant class.

Notes for command handlers

  • Provision Create Tenant with LifecycleStatus=Provisioning, set Profile, RegionResidency (region/silo). Optionally set EditionDefaultsRef. Emit tenant.created.
  • Activate Validate invariants (residency set, owner contact, domain uniqueness). If policy requires, set ResidencyLocked=true and LockedAtUtc=now. Emit tenant.activated and tenant.edition_defaults_applied (if defaults set).
  • Suspend/Reinstate Flip status and emit events; ensure services consult status (tokens/requests denied for Suspended).
  • Request Deletion / Delete Transition to PendingDeletion, run retention/export workflows, then finalize to Deleted with archival references. Emit tenant.deletion_requested and tenant.deleted.

Suggested supporting enumerations (this context)

  • TenantLifecycleStatus { Provisioning = 0, Active = 1, Suspended = 2, PendingDeletion = 3, Deleted = 4 }
  • ContactRole { Owner = 1, Billing = 2, Technical = 3, Support = 4, Security = 5 }

These contracts establish Tenant as the authoritative issuer of tenantId, define owned identity/residency VOs, and set clear lifecycle, invariants, and events to coordinate Config, Identity, Billing, and Observability—without leaking cross-context navigations.


Relationship diagrams & repository specs (Catalog & Tenant)

Relationship map (navigations)

erDiagram
  Product ||--o{ Edition : "HasMany" 
  Edition }o--o{ Feature : "HasManyThrough via EditionFeature"
  Edition ||--o{ EditionSla : "HasMany (M:N via join)"
  ServiceLevelAgreement ||--o{ EditionSla : "Inverse"
  PricingModel ||--o{ EditionPricingModel : "HasMany"
  Edition ||--o{ EditionPricingModel : "Inverse"
  BusinessModel }o--o{ Product : "M:N"
  BusinessModel }o--o{ Edition : "M:N"
  BusinessModel }o--o{ Feature : "M:N"
  BusinessModel }o--o{ ServiceLevelAgreement : "M:N"
  BusinessModel }o--o{ PricingModel : "M:N"
  Feature ||--o{ BillingRule : "HasMany"
  Tenant ||--o{ Contact : "HasMany"
  Tenant }o--|| Edition : "Default Edition (optional Reference)"
Hold "Alt" / "Option" to enable pan & zoom
  • Product 1→* Editions (Edition carries Product reference).
  • Edition M↔N Feature via EditionFeature (explicit link entity).
  • Edition 1→* SLA (modeled as M:N join; only one active at a time).
  • PricingModel 1→* EditionPricingModel → Edition (effective-dated per-edition price).
  • BusinessModel M↔N Products/ Editions/ Features/ SLAs/ PricingModels; Reference → DefaultPricingModel.
  • Feature 1→* BillingRule (feature-scoped rating).
  • Tenant 1→* Contact, and Tenant (optional) ref Default Edition used to seed config. (Specified in prior cycle.)

Repository interfaces (spec-only)

Repositories return aggregate roots or thin projections. They avoid leaking ORMs to callers, and expose identity-only and key-based loaders. Navigation graphs are loaded explicitly via well-named methods to avoid N+1.

Catalog — ProductRepository

  • Task<IProduct?> GetByIdAsync(Guid productId, CancellationToken ct)
  • Task<IProduct?> GetByKeyAsync(string productKey, CancellationToken ct)
  • Task<bool> KeyExistsAsync(string productKey, CancellationToken ct)
  • Task<IReadOnlyList<IProduct>> ListPublishedAsync(CancellationToken ct)
  • Task<IReadOnlyList<IEdition>> LoadEditionsAsync(Guid productId, CancellationToken ct)
  • Task PublishAsync(Guid productId, string version, CancellationToken ct) (enforces “≥1 published edition” invariant)

Catalog — EditionRepository

  • Task<IEdition?> GetByIdAsync(Guid editionId, CancellationToken ct)
  • Task<IEdition?> GetByKeyAsync(Guid productId, string editionKey, CancellationToken ct)
  • Task<IReadOnlyList<IEditionFeature>> LoadEditionFeaturesAsync(Guid editionId, DateTime? atUtc, CancellationToken ct)
  • Task<IServiceLevelAgreement?> GetActiveSlaAsync(Guid editionId, DateTime atUtc, CancellationToken ct)
  • Task<IEditionPricingModel?> GetActivePriceAsync(Guid editionId, Guid pricingModelId, DateTime atUtc, CancellationToken ct)
  • Task PublishAsync(Guid editionId, string compositionVersion, CancellationToken ct)

Catalog — FeatureRepository

  • Task<IFeature?> GetByIdAsync(Guid featureId, CancellationToken ct)
  • Task<IFeature?> GetByKeyAsync(string featureKey, CancellationToken ct)
  • Task<IReadOnlyList<IBillingRule>> LoadBillingRulesAsync(Guid featureId, DateTime? atUtc, CancellationToken ct)
  • Task<IReadOnlyList<IEditionFeature>> LoadEditionLinksAsync(Guid featureId, CancellationToken ct)
  • Task<IReadOnlyList<IEditionFeature>> ListByEditionAsync(Guid editionId, DateTime? atUtc, CancellationToken ct)
  • Task<bool> HasOverlapAsync(Guid editionId, Guid featureId, DateTime effective, DateTime? expiry, CancellationToken ct) (prevent overlapping windows)

Catalog — ServiceLevelAgreementRepository

  • Task<IServiceLevelAgreement?> GetByIdAsync(Guid slaId, CancellationToken ct)
  • Task<IServiceLevelAgreement?> GetByKeyAsync(string slaKey, CancellationToken ct)
  • Task<IServiceLevelAgreement?> GetActiveForEditionAsync(Guid editionId, DateTime atUtc, CancellationToken ct)
  • Task<bool> SetActiveForEditionAsync(Guid editionId, Guid slaId, DateTime effective, DateTime? expiry, CancellationToken ct) (enforce “one active”)

Catalog — BusinessModelRepository

  • Task<IBusinessModel?> GetByIdAsync(Guid businessModelId, CancellationToken ct)
  • Task<IReadOnlyList<IProduct>> LoadProductsAsync(Guid businessModelId, CancellationToken ct)
  • Task<IReadOnlyList<IEdition>> LoadEditionsAsync(Guid businessModelId, CancellationToken ct)
  • Task<IReadOnlyList<IFeature>> LoadFeaturesAsync(Guid businessModelId, CancellationToken ct)
  • Task<IReadOnlyList<IServiceLevelAgreement>> LoadSlasAsync(Guid businessModelId, CancellationToken ct)
  • Task<IReadOnlyList<IPricingModel>> LoadPricingModelsAsync(Guid businessModelId, CancellationToken ct)
  • Task<IPricingModel?> GetDefaultPricingModelAsync(Guid businessModelId, CancellationToken ct)

Catalog — PricingModelRepository

  • Task<IPricingModel?> GetByIdAsync(Guid pricingModelId, CancellationToken ct)
  • Task<IReadOnlyList<IEditionPricingModel>> ListEditionPricesAsync(Guid pricingModelId, DateTime? atUtc, CancellationToken ct)

Catalog — EditionPricingModelRepository

  • Task<IEditionPricingModel?> GetActiveAsync(Guid editionId, Guid pricingModelId, DateTime atUtc, CancellationToken ct)
  • Task<bool> HasOverlapAsync(Guid editionId, Guid pricingModelId, DateTime effective, DateTime? expiry, CancellationToken ct)

Catalog — BillingRuleRepository

  • Task<IBillingRule?> GetByIdAsync(Guid billingRuleId, CancellationToken ct)
  • Task<IReadOnlyList<IBillingRule>> ListActiveForFeatureAsync(Guid featureId, DateTime atUtc, CancellationToken ct)
  • Task<bool> HasOverlapAsync(Guid featureId, BillingCycleEnumeration cycle, DateTime effective, DateTime? expiry, CancellationToken ct)

Tenant — TenantRepository

  • Task<ITenant?> GetByIdAsync(Guid tenantId, CancellationToken ct)
  • Task<bool> DomainExistsAsync(string domain, CancellationToken ct)
  • Task<IReadOnlyList<IContact>> LoadContactsAsync(Guid tenantId, CancellationToken ct)
  • Task SetDefaultEditionAsync(Guid tenantId, Guid? editionId, CancellationToken ct)
  • Task<bool> ActivateAsync(Guid tenantId, DateTime atUtc, CancellationToken ct) (enforces residency + owner contact)
  • Task SuspendAsync(Guid tenantId, string reason, CancellationToken ct) / Task ReinstateAsync(Guid tenantId, CancellationToken ct)
  • Task RequestDeletionAsync(Guid tenantId, DateTime earliestDelete, CancellationToken ct) / Task FinalizeDeletionAsync(Guid tenantId, CancellationToken ct)

Loading guidance (eager vs. lazy)

  • Identity-only loads by default: methods named GetById* / GetByKey* return just the aggregate; callers explicitly request navigations.
  • Targeted includes: use specific methods like LoadEditionFeaturesAsync, LoadBillingRulesAsync, GetActivePriceAsync to avoid accidental deep graphs (prevents N+1 and large payloads).
  • Time-aware queries: where effective dating exists (EditionFeature, EditionPricingModel, BillingRule, SLAs), provide atUtc to resolve current data efficiently via indexed predicates.
  • No cross-context navigations: IDs/keys only across bounded contexts; build projections for read scenarios that span contexts (see previous cycle on projections).

Consistency notes

  • One command → one aggregate: mutations touch a single aggregate root; cross-aggregate work is coordinated via domain events and process managers.
  • Navigations are read conveniences: collections like Product.Editions exist for readability inside Catalog; write flows still validate invariants locally.
  • Event-first integration: publish product.*, edition.*, feature.*, pricing.* events; downstream contexts (Identity, Config, Billing) maintain projections rather than calling into Catalog synchronously.
  • Effective-dated correctness: all joins with temporal semantics enforce non-overlap and expose helpers like HasOverlapAsync to keep invariants honest.

This gives you a precise, DDD- and ORM-aware map of how the Catalog and Tenant aggregates relate, plus repository contracts that encourage identity-first loads, explicit navigation fetching, and clean cross-context boundaries.


Real examples: applying the model end-to-end

Below are concrete, production-style examples that exercise the Catalog (Product/Edition/Feature), Pricing, SLAs, Billing Rules, Access Rules, and Tenant Management. Values (keys, quotas, windows) are chosen to align with the enumerations and invariants we defined earlier. These are domain objects, not database rows; effective-dating and joins are implied by the relationships we’ve modeled.


Example A — “ConnectSoft CRM” (subscription + per-feature usage)

Product

Field Example
Product Key: crm-suiteDisplayName: “ConnectSoft CRM” • Status: Published
Notes Product key immutable; at least one published edition required for publish.

Editions

Edition (Key) Status CompositionVersion Notes
Free (free) Published 1.0.0 Lead capture & basic contacts only.
Standard (standard) Published 1.3.0 Adds campaigns, API, and webhooks.
Enterprise (enterprise) Published 2.1.0 SSO, audit trails, higher limits, premium SLA.

Features (selected)

Feature (Key) DisplayName DefaultQuota (VO)
contacts.core Contacts Unlimited (LimitType: Users/Seats not applicable; functional)
campaigns.email Email Campaigns 50,000 messages / Monthly
api.core Public API 1,000,000 API calls / Monthly
webhooks.outbound Outbound Webhooks 500,000 requests / Monthly
sso.saml SSO (SAML) N/A (gated by access rules, not counters)
audit.trail Audit Trail Retention bound by SLA, not quota

EditionFeature composition (effective today)

Edition FeatureKey Mode QuotaOverride?
Free contacts.core Enabled
Free campaigns.email Enabled 5,000 messages / Monthly
Standard campaigns.email Enabled 200,000 messages / Monthly
Standard api.core Enabled 2,000,000 API calls / Monthly
Standard webhooks.outbound Enabled 200,000 requests / Monthly
Enterprise api.core Enabled 10,000,000 API calls / Monthly
Enterprise webhooks.outbound Enabled 2,000,000 requests / Monthly
Enterprise sso.saml Enabled
Enterprise audit.trail Enabled

SLAs

Edition SLA Availability SupportWindow Notes
Free sla.standard v1 99.5% Community/Email (9×5) Single queue
Standard sla.standard v2 99.9% 9×5 P2 4h response
Enterprise sla.enterprise v3 99.95% 24×7 P1 1h response, credits policy v3

Pricing

  • BusinessModel: bm.crm.default (Published)
  • PricingModel (default): pm.crm.subscription

  • PricingType: subscription • Currency: USD • BillingCycle: monthly

  • BasePrice: 39 • Min: 39 • Max: 39 (Free has 0 via edition price)
  • EditionPricingModel (effective now):

  • Free: Base 0, Min 0, Max 0

  • Standard: Base 39
  • Enterprise: Base 99

Billing Rules (feature-level, effective now)

FeatureKey Rule Cycle Threshold OverageRate Bounds
campaigns.email “Email Overage” Monthly 200,000 msgs 0.0008/msg Min 0, Max 5,000
api.core “API Overage” Monthly 2,000,000 calls 0.00001/call Min 0, Max 2,000
webhooks.outbound “Webhook Overage” Monthly 200,000 req 0.00002/req Min 0, Max 1,000

Access rules (examples)

  • Rule: feature.api.manage_keysAccessScope=Feature, AccessType=Manage, Condition: role in ["Admin","Owner"].
  • Exception: narrow to tenant security.tier="strict" → deny Manage unless MFA present (ABAC predicate).

Example B — “Doc Intelligence” (AI usage, rolling windows, PAYG hybrid)

Product & Editions

  • Product: ai-doc-intel (Published)
  • Editions: starter (Published), pro (Published)

Features & quotas

FeatureKey DefaultQuota
ai.tokens 5,000,000 tokens / Rolling 24 Hours
ai.batch_extract 100,000 pages / Monthly
ai.private_models N/A (gated by edition)

EditionFeature overrides

  • starter: ai.tokens → 1,000,000 / Rolling 24h; ai.batch_extract → 10,000 / Monthly
  • pro: inherits defaults; ai.private_models → Mode=Preview (no SLA)

Pricing (hybrid)

  • PricingModel: pm.ai.hybrid — PricingType: hybrid (subscription base + usage)

  • Base (Starter/Pro): 0 / 299 (monthly)

  • Billing Rules

  • ai.tokens: threshold 5M / Rolling 24h equivalent (rated against monthly bill), overage 0.000002 per token, cap 10,000

  • ai.batch_extract: threshold 100k / Monthly, overage 0.01 per page, cap 5,000

SLA

  • Starter: sla.ai.standard 99.5% (no credits for preview)
  • Pro: sla.ai.premium 99.9% (credits apply)

Example C — Tenant lifecycle using these products

Tenant at a glance

Field Value
TenantId c7a0…-acme-eu
Profile LegalName=Acme GmbH, DisplayName=Acme EU, Domain=acme-eu.connectsoft.cloud
RegionResidency RegionCode=EU, DataSiloId=eu-shard-03, ResidencyLocked=true (on activation)
EditionDefaultsRef Edition( crm-suite / enterprise )
Contacts Owner=sara@acme.eu, Billing=ap@acme.eu, Technical=ops@acme.eu
LifecycleStatus Active (since 2025-02-10T09:00Z)

Activation flow (events)

  1. tenant.created → payload includes profile + region.
  2. tenant.activated (+ tenant.residency_locked) → emits with EditionDefaultsRef.
  3. Catalog emits edition.published/edition.changed as needed; Config composes tenant-level overrides.
  4. Config/Identity emit entitlements.changed with digest → short-lived tokens enriched with edition + feature claims.

Example D — From navigations to read models (DTOs)

CatalogView (excerpt)

{
  "productKey": "crm-suite",
  "editions": [
    {
      "editionKey": "standard",
      "compositionVersion": "1.3.0",
      "features": [
        { "key": "campaigns.email", "mode": "enabled", "quota": { "limit": 200000, "limitType": "messages", "resetPeriod": "monthly" } },
        { "key": "api.core", "mode": "enabled", "quota": { "limit": 2000000, "limitType": "api_calls", "resetPeriod": "monthly" } }
      ],
      "sla": { "key": "sla.standard", "version": "2" }
    }
  ]
}

EntitlementsView (tenant-scoped)

{
  "tenantId": "c7a0…-acme-eu",
  "editionKey": "enterprise",
  "features": [
    { "key": "api.core", "state": "enabled", "quota": { "limit": 10000000, "limitType": "api_calls", "resetPeriod": "monthly" } },
    { "key": "webhooks.outbound", "state": "enabled", "quota": { "limit": 2000000, "limitType": "requests", "resetPeriod": "monthly" } },
    { "key": "sso.saml", "state": "enabled" },
    { "key": "audit.trail", "state": "enabled" }
  ],
  "digest": "9b6b9e80…",
  "updatedAtUtc": "2025-09-29T10:12:00Z"
}

What these examples demonstrate

  • Aggregate boundaries: Product→Editions; Edition→EditionFeature links; Feature-centric rules; SLA attached via join with “one active” invariant.
  • Enumerations in action: BillingCycle=monthly, PricingType=subscription/hybrid, ResetPeriod=monthly/rolling_24h, LimitType=api_calls/messages.
  • Invariants upheld: Only Published features included in published editions; non-overlapping effective windows for EditionFeature and pricing; min≤base≤max for prices.
  • Event-first: Minimal, key-oriented events flow into CatalogView and EntitlementsView; tokens carry snapshots by short names.
  • Tenant alignment: Residency locked before activation; default edition seeding; contacts for approvals and notifications.

Subscription & Licensing Context

Relationships

  • Reference → Edition (Catalog) Each subscription targets a single edition at a time (by EditionId or EditionKey). No cross-context navigations—store the ID/key.
  • HasMany → SeatAssignments Seats are assigned to users or service principals in Identity; this context only tracks the assignment facts (principal IDs + dates).
  • HasOne → LicensePool (inside the Subscription aggregate) Capacity (total seats, grace, over-allocation policy) lives with the subscription to keep seat invariants strongly consistent.
  • HasMany → AddOnPacks Purchased add-ons bound to the subscription; each has quantity and effective dating.

Note: You can later “promote” LicensePool or AddOnPack to separate aggregates if you need cross-subscription pooling or independent lifecycles. Default design keeps them inside Subscription to honor “one command → one aggregate”.


Types (aggregates / entities / VOs)

  • Aggregates: ISubscription
  • Entities (child): ILicensePool, ISeatAssignment, IAddOnPack
  • VOs: SubscriptionTerm, RenewalPolicy, BillingAnchor, AddOnRef
  • Enums: SubscriptionStatus, PaymentState, PrincipalType

ISubscription — properties (contract + semantics)

Property Type Description & rules
SubscriptionId Guid Aggregate identifier; stable and opaque. Used as partition key in this context.
TenantId Guid Owning tenant. Required on every command and event for RLS.
EditionRef Guid (EditionId) or string (EditionKey) Target edition from Catalog at this point in time. Edition changes are governed by RenewalPolicy.
Status SubscriptionStatus (Trial, Active, PastDue, Suspended, Canceled, Expired) Commercial lifecycle. Canceled and Expired are terminal for service; reactivation opens a new term.
PaymentState PaymentState (Trialing, Paid, PastDue, Dunning, Failed, Free) Payment processor-facing state; drives grace periods and webhooks to Billing.
Term SubscriptionTerm (VO) Contract window and edition entitlement window.
Renewal RenewalPolicy (VO) Whether/how the subscription renews (auto-renew, alignment, proration).
BillingAnchor BillingAnchor (VO) Cadence alignment for invoicing (e.g., monthly on the 1st, 09:00 UTC).
LicensePool ILicensePool Seats capacity & policy inside the aggregate to keep seat invariants strong.
SeatAssignments IList<ISeatAssignment> Occupied seats (users/service principals). Derived count must not exceed LicensePool.TotalSeats (unless over-allocation grace is enabled).
AddOnPacks IList<IAddOnPack> Purchased add-ons with quantities and effective windows (e.g., extra storage, API pack).
CreatedAtUtc DateTime Creation time.
ActivatedAtUtc DateTime? Transition to Active. Set only once.
CanceledAtUtc DateTime? Transition to Canceled.
NextRenewalAtUtc DateTime? Next renewal timestamp (if Renewal.AutoRenew=true).
TrialEndsAtUtc DateTime? Trial cutoff (if Status=Trial).

SubscriptionTerm (VO)

Property Type Description & rules
StartAtUtc DateTime Start of the contract/entitlement window (inclusive).
EndAtUtc DateTime? End (exclusive). null for open-ended (renewing) terms.
BillingCycle BillingCycleEnumeration Recurrence cadence (monthly/annual/…).
TrialDays int? Optional trial length; TrialEndsAtUtc = StartAtUtc + TrialDays. Must be ≥ 0.

RenewalPolicy (VO)

Property Type Description & rules
AutoRenew bool If true, extend term at NextRenewalAtUtc automatically.
ProrateOnUpgrade bool When upgrading edition or increasing seats mid-cycle, compute proration.
ChangeEditionAtRenewalOnly bool If true, edition changes are queued and applied at next renewal.
GracePeriodDays int Allowed grace post PastDue before suspension. Non-negative.

BillingAnchor (VO)

Property Type Description & rules
AnchorDay int Day-of-month for renewal (1–28, or 31 with “last-day” semantics).
AnchorHourUtc int Hour of day (0–23) to cut over.
ProrationBasis string (calendar anniversary)

ILicensePool (entity inside Subscription)

Property Type Description & rules
TotalSeats int Maximum assignable seats for this subscription. Must be ≥ 0.
OverAllocationGrace int Temporary excess seats allowed (e.g., 5) before enforcement; used during bulk changes.
UsedSeats int (derived) Count of active seat assignments. Cannot exceed TotalSeats + OverAllocationGrace.
ReservedSeats int Seats reserved for pending invites/provisioning. Must be ≥ 0 and Used + Reserved ≤ Total + Grace.

ISeatAssignment (entity)

Property Type Description & rules
SeatAssignmentId Guid Identifier for audit/updates.
PrincipalId Guid User or Service Principal Id from Identity.
PrincipalType PrincipalType (User, ServicePrincipal) Distinguishes who consumes the seat.
RoleKey string? Optional role shortcut granted with the seat (e.g., app.user). Binding to actual roles happens in Identity.
AssignedAtUtc DateTime When the seat became occupied.
UnassignedAtUtc DateTime? When released; null means currently occupying.

IAddOnPack (entity)

Property Type Description & rules
AddOnPackId Guid Identifier.
AddOn AddOnRef (VO) Points to the add-on definition (by key) in Catalog (or an AddOn catalog).
Quantity int Purchased units; must be ≥ 1.
EffectiveDate DateTime Start (inclusive).
ExpiryDate DateTime? End (exclusive); null = open-ended while subscription active.
IsActive bool Convenience flag; true iff now within window and subscription is Active.

AddOnRef (VO)

Property Type Description & rules
Key string Canonical add-on key (slug).
DisplayName string Human label; snapshot for reads.
FeatureKey? string? If the add-on maps to a Feature, include the feature key for entitlements/metering.

Enums

Enum Members
SubscriptionStatus Trial, Active, PastDue, Suspended, Canceled, Expired
PaymentState Trialing, Paid, PastDue, Dunning, Failed, Free
PrincipalType User, ServicePrincipal

Invariants

  • Seat capacity: UsedSeats + ReservedSeats ≤ TotalSeats + OverAllocationGrace. Assigning a seat must fail if capacity would be exceeded (unless a grace policy explicitly allows it).
  • Edition change discipline: If Renewal.ChangeEditionAtRenewalOnly=true, any edition change command records a pending change and applies it at NextRenewalAtUtc. If false, apply immediately with proration if Renewal.ProrateOnUpgrade=true.
  • Trial → Active: Requires: verified Owner contact on tenant, and a valid payment method if the plan is paid (PaymentState=Paid).
  • Temporal coherence: EffectiveDate ≤ ExpiryDate on add-ons; AssignedAtUtc ≤ UnassignedAtUtc for seat history.
  • Tenant/Edition existence: TenantId must reference an existing, non-deleted tenant; EditionRef must reference a Published edition (by id/key) at activation time.

Domain events

  • subscription.created{ subscriptionId, tenantId, editionKey|id, status, term, renewal, billingAnchor }
  • subscription.activated{ subscriptionId, activatedAtUtc }
  • subscription.renewed{ subscriptionId, previousTermEnd, newTermEnd, editionKey|id }
  • subscription.changed{ subscriptionId, changes: { seats?, edition?, renewal? } }
  • subscription.canceled{ subscriptionId, canceledAtUtc, reason }
  • seat.assigned / seat.unassigned{ subscriptionId, seatAssignmentId, principalId, principalType }
  • addon.added / addon.removed{ subscriptionId, addOnPackId, addOnKey, quantity, effectiveDate, expiryDate? }

All events follow the platform envelope (id, time, traceId, schemaVersion) and are partitioned by tenantId.


APIs / projections

Commands

  • CreateSubscription(tenantId, editionRef, term, renewal, billingAnchor, totalSeats, trialDays?)
  • ActivateSubscription(subscriptionId)
  • ChangeEdition(subscriptionId, newEditionRef, applyNow|AtRenewal)
  • AdjustSeats(subscriptionId, newTotalSeats)
  • AssignSeat(subscriptionId, principalId, principalType, roleKey?)
  • UnassignSeat(subscriptionId, seatAssignmentId)
  • AddAddOn(subscriptionId, addOnKey, quantity, effectiveDate, expiryDate?)
  • RemoveAddOn(subscriptionId, addOnPackId)
  • CancelSubscription(subscriptionId, when, reason)

Reads / Projections

  • SubscriptionView: { subscriptionId, tenantId, editionKey, status, paymentState, term {start,end}, renewal {autoRenew,…}, license { total, used, reserved }, addons[], nextRenewalAtUtc }
  • SeatRoster: list of active seat assignments with principal types.
  • PendingChangesView: queued edition/seat changes for next renewal.

Integration

  • Emit events to Billing (rating/invoicing), Identity (grant/revoke role bundles on seat assign/unassign), and Config/Entitlements (apply add-on feature keys).

ORM notes

  • Aggregate mapping: Subscription root table (PK = SubscriptionId), with owned VO columns for Term, Renewal, BillingAnchor.
  • Child tables:

  • SeatAssignment(SeatAssignmentId PK, SubscriptionId FK, PrincipalId, PrincipalType, AssignedAtUtc, UnassignedAtUtc?)

  • AddOnPack(AddOnPackId PK, SubscriptionId FK, AddOnKey, Quantity, EffectiveDate, ExpiryDate?, IsActive)
  • Indexes:

  • SeatAssignment on (SubscriptionId, PrincipalId, UnassignedAtUtc IS NULL) for “currently assigned” queries.

  • AddOnPack on (SubscriptionId, IsActive, EffectiveDate DESC) for current add-ons.
  • Checks: DB constraints for non-negative seats/quantities; application-enforced invariant for capacity and edition change discipline.
  • No cross-context navigation: Store EditionRef as scalar key/ID; join via projections if needed.

Handler guidance (consistency)

  • One command → one aggregate: adjust seats and assign/unassign through Subscription.
  • Seat assignment flow: load Subscription with LicensePool and current SeatAssignments (active) → validate capacity → append assignment → emit seat.assigned.
  • Edition change flow: if apply-now, emit subscription.changed (+ entitlements.changed downstream); if at-renewal, record pending change and schedule at NextRenewalAtUtc.
  • PastDue → Suspended: consumer of Billing events updates PaymentState; a policy transitions Status after Renewal.GracePeriodDays.

This context keeps licensing decisions and seat capacity inside the Subscription aggregate, ensuring strong invariants, clean events to Billing/Identity, and crystal-clear separation from Catalog (keys/ids only, never navigations).


Provisioning & Environment Context

Relationships

  • ProvisioningRun HasMany Steps A run is an execution plan made of ordered, dependency-aware steps.
  • ProvisioningRun HasMany ResourceBindings Each successful step can register one or more resource bindings (what was created/updated/deleted).
  • ProvisioningRequest → ProvisioningRun A request may spawn exactly one authoritative run (idempotent by ProvisioningRequestId). Retries resume/replace the same run depending on policy.

Types in this context

  • Aggregates: IProvisioningRequest, IProvisioningRun, IResourceBinding
  • Entities/VOs: IStep (entity), IStepLog (entity), TemplateRef (VO)
  • Enums: RunStatus, StepStatus, ResourceState

Interface names are provided to standardize contracts; concrete classes can add behavior but must not alter the semantics below.


IProvisioningRequest — properties (contract + semantics)

Property Type Description & rules
ProvisioningRequestId Guid Idempotency key supplied by the caller (or generated and echoed). Exactly one authoritative run is allowed per id.
TenantId Guid Target tenant. Must refer to an existing, non-deleted tenant.
Template TemplateRef (VO) Which provisioning template/version to execute (golden image, infra recipe).
Variables IDictionary<string,string> Name-value inputs to parameterize the template (region, sku, capacity). Secrets must be provided as references only.
RequestedRegion string Requested data region (e.g., EU, US). Must be compatible with Tenant residency policy.
RequestedSiloId? string? Optional explicit silo/shard. If omitted, placement engine selects one that satisfies capacity & policy.
DryRun bool When true, perform validation/plan only (no side effects).
RequestedAtUtc DateTime Submission timestamp.
CorrelationId string Cross-service correlation/tracing id.

Notes

  • A ProvisioningRequest is immutable after acceptance; later corrections produce a new request id.

IProvisioningRun — properties

Property Type Description & rules
ProvisioningRunId Guid Aggregate id for the run.
ProvisioningRequestId Guid Back-reference to the request that created this run; enforces idempotency.
TenantId Guid Redundant for partition/RLS.
Status RunStatus (Planned, Running, Succeeded, Failed, PartiallyCompensated, Compensated, Aborted) Current lifecycle state.
PlannedAtUtc / StartedAtUtc / CompletedAtUtc? DateTime Timestamps for lifecycle transitions.
Template TemplateRef (VO) Snapshot of template key/version used.
ResolvedRegion string Final region after policy/placement resolution. Must match Tenant residency.
ResolvedSiloId string Final silo/shard chosen.
Steps IList<IStep> Ordered plan with dependencies and retries.
ResourceBindings IList<IResourceBinding> Produced resources (ids, providers, types, states).
FailureSummary? string? Compact description of the failing step(s) and error categories.
CompensationPlan? IList<IStep> Reverse-ordered steps to undo side effects (generated when needed).

IStep — properties (child entity)

Property Type Description & rules
StepId Guid Identity for audit/resume.
Index int Order within the plan. Combined with DependsOn for scheduling.
Name string Human-friendly identifier (e.g., CreateTenantDb).
ActionKey string Handler/action slug (e.g., sql.create_database, kv.put_secret_ref).
Parameters IDictionary<string,string> Materialized inputs for this step (secrets by reference only).
DependsOn IReadOnlyList<Guid> Step ids that must succeed before this step is eligible.
Attempt int Current attempt count (starts at 0).
MaxAttempts int Maximum retry attempts (e.g., 3 with backoff).
IdempotencyKey string Deterministic id for the side effect; ensures exactly-once at the provider level.
Status StepStatus (Pending, Running, Succeeded, Failed, Compensating, Compensated, Skipped) Current state.
StartedAtUtc? / EndedAtUtc? DateTime? Timing for the current/last attempt.
Logs IList<IStepLog> Structured logs & important outputs (resource ids, endpoints).

IStepLog — properties (child entity)

Property Type Description & rules
TimestampUtc DateTime When the log was recorded.
Level string (Info,Warn,Error,Debug) Severity.
Message string Human-readable message.
Data? IDictionary<string,string>? Structured fields (provider request ids, latency ms). No secrets.

TemplateRef (VO)

Property Type Description
Key string Template identifier (e.g., tenant.basic.v2).
Version string SemVer of the template logic.
Checksum string Hash of the compiled/template plan to guarantee reproducibility.

IResourceBinding — properties (aggregate)

Property Type Description & rules
ResourceBindingId Guid Aggregate id.
ProvisioningRunId Guid Run that created/updated this resource.
TenantId Guid Owning tenant (partition/RLS).
Provider string Provider slug (azure, aws, gcp, internal-svc).
ResourceType string Type slug (sql-db, kv-secret, blob-bucket, routing-entry).
ResourceId string Provider-native id or fully qualified name.
Region string Physical region where resource resides.
SiloId string Local shard/cluster identifier.
State ResourceState (Active, Updating, Compensating, Decommissioned) Lifecycle state.
Tags IReadOnlyList<KeyValueTag> Metadata for discovery/ops.
CreatedAtUtc / UpdatedAtUtc DateTime Lifecycle timestamps.

Enums

Enum Members
RunStatus Planned, Running, Succeeded, Failed, PartiallyCompensated, Compensated, Aborted
StepStatus Pending, Running, Succeeded, Failed, Compensating, Compensated, Skipped
ResourceState Active, Updating, Compensating, Decommissioned

Invariants & policies

  • Exactly-once per request A ProvisioningRequestId may have at most one authoritative ProvisioningRun in non-terminal state. Replays with the same id resume the existing run.
  • Tenant residency match ResolvedRegion must equal the tenant’s RegionResidency.RegionCode; ResolvedSiloId must be a registered silo for that region. Reject otherwise.
  • Step dependency & retries A step may run only when all DependsOn are Succeeded. Retries increment Attempt up to MaxAttempts with backoff; after that the run enters Failed (and may generate a compensation plan).
  • Idempotent effects Every external call uses IdempotencyKey (e.g., provider idempotency token) to guarantee at-least-once engine → exactly-once side effects.
  • Secrets handling Steps only receive references (URIs/ids) to secrets; retrieval happens via the Secrets context with short-lived tokens. No raw secrets are persisted in steps/logs/events.
  • Compensation If any step fails irrecoverably, generate reverse-order compensation steps for already-succeeded steps that registered side effects, transitioning the run to PartiallyCompensated or Compensated.

Domain events

  • provisioning.started{ runId, requestId, tenantId, template: { key, version }, region, siloId }
  • provisioning.step_succeeded{ runId, stepId, name, actionKey, attempt, durationMs }
  • provisioning.step_failed{ runId, stepId, name, actionKey, attempt, errorCode, errorMessage, retryable }
  • provisioning.completed{ runId, status: "Succeeded"|"Failed"|"Compensated", resourceCount }
  • provisioning.compensated{ runId, compensatedSteps, status: "Compensated" }
  • resource.binding.created|updated|decommissioned{ bindingId, runId, tenantId, provider, type, resourceId, region, siloId, state }

(All events use the platform envelope, are partitioned by tenantId, and correlate via runId and requestId.)


APIs / projections

Commands

  • StartProvisioning(requestId, tenantId, templateRef, variables, requestedRegion, requestedSiloId?, dryRun=false)
  • AbortRun(runId, reason) — attempts graceful stop; marks remaining steps Skipped.
  • RetryRun(runId) — restarts failed steps if retryable; preserves succeeded steps.
  • RetryStep(runId, stepId) — targeted retry (ops workflow).
  • ApplyCompensation(runId) — executes generated compensation plan.
  • ReconcileBindings(tenantId) — scans provider state vs. bindings and fixes drift.

Reads

  • ProvisioningStatusView

{
  runId, requestId, tenantId, status, template:{key,version},
  region, siloId, startedAtUtc, completedAtUtc?,
  steps:[{ stepId,index,name,actionKey,status,attempt,maxAttempts,startedAtUtc?,endedAtUtc?,dependsOn[] }],
  resourceBindings:[{ bindingId, provider, resourceType, resourceId, region, siloId, state }],
  failureSummary?
}
* RecentRunsView(tenantId) — last N runs with status rollups. * ResourceBindingsView(tenantId) — current active bindings per tenant.


ORM notes

  • Aggregates & ownership

  • ProvisioningRun as root; Steps and StepLogs as child tables (FK RunId).

  • ResourceBinding as a separate aggregate (own table) with FK to RunId for provenance; can be mutated independently (e.g., decommission).
  • Uniqueness & indexes

  • Unique index on ProvisioningRequestId in ProvisioningRun (enforces idempotency).

  • Composite indexes: Steps(RunId, Status, Index), ResourceBinding(TenantId, State, ResourceType).
  • Optional unique on (Provider, ResourceId) to prevent duplicate bindings.
  • Concurrency

  • Use optimistic concurrency on ProvisioningRun and per-step rows to avoid double-execution.

  • A lightweight lease (e.g., LockOwner, LockExpiresAt) can coordinate workers for the same run.
  • Retention

  • Keep StepLog for 30–90 days (configurable); keep ResourceBinding until decommission + grace.


Handler guidance (engine behavior)

  • Planner builds the DAG of Steps from TemplateRef + Variables, validates tenancy/residency/capacity, and persists the plan (RunStatus=Planned).
  • Runner picks ready steps (no unmet deps, not running), acquires a lease, sets Status=Running, executes with idempotency, writes StepLog, and transitions to Succeeded or Failed.
  • Retry logic respects MaxAttempts with exponential backoff and error classification (retryable vs fatal).
  • Compensator generates reverse operations for steps that registered side effects (e.g., drop DB, delete bucket, remove routing).
  • Auditing: every state transition emits events and a structured audit record with traceId and step outputs (without secrets).

Integration touchpoints

  • Tenant: on tenant.activated, start a provisioning request (if not already provisioned). On successful completion, update routing (region/silo).
  • Data Residency & Routing: consume resource.binding.* to build the RoutingTable for gateways.
  • Secrets: resolve secret references at step execution time (short-lived credentials).
  • Notifications: post progress to Owners/Tech contacts; attach ProvisioningStatusView deep-link.

This context gives you a deterministic, idempotent, and compensating engine to stand up (and tear down) tenant environments while staying faithful to residency policies and auditability requirements.


Data Residency & Routing Context

Relationships

  • RegionPolicy HasMany DataSilo A region (e.g., EU, US) owns its set of silos/clusters and the rules for placing tenants within it.
  • DataSilo HasMany ShardAssignment Each silo carries assignments (by tenant, hash range, or sub-shard) subject to capacity.
  • MigrationPlan References (from→to) DataSilo A plan describes a tenant’s move between silos (same region unless policy allows cross-region with approvals).

Types in this context

  • Aggregates: IRegionPolicy, IDataSilo, IShardAssignment
  • Entities/VOs: IMigrationPlan (entity), CapacityPlan (VO), EndpointSet (VO), ResidencyRule (VO)
  • Enums (suggested): SiloState { Active, Draining, ReadOnly, Decommissioned }, MigrationStatus { Planned, Approved, Running, Completed, Failed, Canceled }

Names are interface-oriented; implement as aggregates with behavior. VOs are owned types in ORM.


IRegionPolicy — properties (contract + semantics)

Property Type Description & rules
RegionCode string Key for the jurisdiction (e.g., EU, US, IL). Immutable identifier.
DisplayName string Human-friendly label (e.g., “European Union”).
ResidencyRules IReadOnlyList<ResidencyRule> Set of predicates/constraints (data locality, allowed providers, cross-border transfer rules).
Silos IList<IDataSilo> HasMany silos governed by this policy.
DefaultPlacementStrategy string Slug of placement algorithm (balanced, least_connections, hash_tenantId).
FailoverRegionCodes IReadOnlyList<string> Ordered list of regions allowed for failover (can be empty/none).
UpdatedAtUtc DateTime Last time policy changed (triggers rebalancing checks).

ResidencyRule (VO)

Property Type Description
Key string Identifier of the rule (slug).
Expression string Policy expression (e.g., CEL/OPA-like) evaluated on tenant.profile, subscription, provider.
Effect string (Allow Deny
Reason string Operator-facing rationale for audits and error details.

IDataSilo — properties

Property Type Description & rules
SiloId string Unique identifier (e.g., eu-shard-03). Immutable.
RegionCode string Back-reference to owning region.
Provider string Infra provider slug (azure | aws | gcp | onprem).
State SiloState Operational state (Active, Draining, …); affects placement and migration.
Capacity CapacityPlan (VO) Max tenants, storage/IO ceilings, connection budget; hard caps.
Endpoints EndpointSet (VO) Public/priv endpoints (read/write/data plane), health probe URIs.
Tags IReadOnlyList<KeyValueTag> Ops metadata (sku, zone, cost-center).
CreatedAtUtc / UpdatedAtUtc DateTime Lifecycle timestamps.

CapacityPlan (VO)

Property Type Description & rules
MaxTenants int Hard cap for tenant count. Must be ≥ 1.
MaxStorageGb int Aggregate storage budget (advisory unless enforced by provider).
MaxIops int Aggregate IOPS budget (advisory).
WarnAtTenants int Soft threshold for alerts/rebalancing. Must be ≤ MaxTenants.

EndpointSet (VO)

Property Type Description
ReadWrite Uri Primary read/write endpoint for the tenant data plane.
ReadOnly Uri? Optional RO endpoint (reporting/BI).
Admin Uri? Control-plane/admin endpoint used by internal services.
Metrics Uri? Observability endpoint for health/telemetry.
DnsZone string? Optional DNS zone suffix (for vanity domains).

IShardAssignment — properties

Property Type Description & rules
AssignmentId Guid Aggregate id.
TenantId Guid Tenant assigned (partition/RLS).
SiloId string Target silo. Must belong to the tenant’s region policy.
RegionCode string Redundant for consistency checks and routing joins.
PlacedAtUtc DateTime When assigned.
PlacementReason string “initial”, “rebalance”, “migration”.
IsPrimary bool Primary placement vs read replica.
ReplicaSiloIds IReadOnlyList<string> Optional read replica silos (within region or policy-approved cross-region).

IMigrationPlan — properties (entity)

Property Type Description & rules
MigrationPlanId Guid Identity for approvals and audit.
TenantId Guid Tenant to move.
FromSiloId string Current silo.
ToSiloId string Target silo. Must be in same RegionCode unless a cross-region policy is explicitly allowed.
Status MigrationStatus Lifecycle of the migration.
PlannedAtUtc / ApprovedAtUtc? / StartedAtUtc? / CompletedAtUtc? DateTime Timeline anchors.
WindowStartUtc / WindowEndUtc DateTime Maintenance window for disruption-minimal cutover.
Method string (online-replication, snapshot-restore, dual-write) Technical approach.
ApprovalBy string? Who approved (user/service) with role.
Notes string? Rationale, risk, rollback plan.

Invariants & policies

  • Silo capacity (hard cap) New ShardAssignment into a silo requires: currentTenants < Capacity.MaxTenants. Reject otherwise unless a formal override is recorded (for emergencies) with audit.
  • Residency lock A tenant with ResidencyLocked=true (from Tenant context) may not change RegionCode. Migrations must remain within the same region, unless an approved cross-region rule exists (and legal basis documented).
  • State-aware placement Placement is allowed only into SiloState=Active. Draining prevents new placements; ReadOnly can serve reads but not new write primaries.
  • Endpoint consistency EndpointSet.ReadWrite must be reachable/healthy before marking an assignment active. Changing endpoints emits endpoint.changed and refreshes RoutingTable.
  • Primary uniqueness A tenant must have exactly one IsPrimary=true assignment at a time. Replicas are optional and cannot be marked primary unless a controlled failover/migration occurs.

Domain events

  • silo.created{ siloId, regionCode, provider, capacity, endpoints }
  • capacity.changed{ siloId, old, @new } (caps and/or warnings)
  • endpoint.changed{ siloId, endpoints }
  • tenant.placement.changed{ tenantId, primary: { siloId, regionCode }, replicas?: [siloId…], reason }
  • migration.planned{ migrationPlanId, tenantId, fromSiloId, toSiloId, window }
  • migration.approved / migration.started / migration.completed / migration.failed — with timestamps and error summaries

(All events use the platform envelope and are partitioned by tenantId where applicable.)


APIs / projections

Commands

  • CreateSilo(regionCode, provider, capacityPlan, endpoints, tags?)
  • UpdateSiloCapacity(siloId, capacityPlan)
  • UpdateSiloEndpoints(siloId, endpoints)
  • AssignTenant(tenantId, regionCode, preferredSiloId?) → computes placement per RegionPolicy.DefaultPlacementStrategy
  • PlanMigration(tenantId, toSiloId, windowStartUtc, windowEndUtc, method, notes?)
  • ApproveMigration(migrationPlanId, approver)
  • StartMigration(migrationPlanId)
  • CompleteMigration(migrationPlanId)
  • FailMigration(migrationPlanId, error)

Reads / Projections

  • RoutingTable (authoritative; cached at edge):

{
  tenantId: "...",
  regionCode: "EU",
  primary: { siloId: "eu-shard-03", endpoints: { readWrite: "...", readOnly: "..." } },
  replicas: [{ siloId: "eu-shard-01", endpoints: {...} }],
  updatedAtUtc: "..."
}
* SiloCatalog: by region/provider/state with capacity utilization (tenants, % used, warn/ok). * MigrationQueue: upcoming/active migrations with windows and status.

Lookup APIs

  • ResolveTenant(tenantId)RoutingTable entry (fast path for gateways/services).
  • FailoverHints(tenantId) → candidate replicas/regions allowed by policy (for read routing or emergency).

ORM notes

  • Aggregates
    • RegionPolicy root table with owned ResidencyRules (JSON or child rows).
    • DataSilo table linked to RegionPolicy via RegionCode FK; owned VOs for CapacityPlan, EndpointSet.
    • ShardAssignment table keyed by AssignmentId with unique (TenantId, IsPrimary WHERE IsPrimary=1) to enforce single-primary.
  • Indexes
    • DataSilo(RegionCode, State) for placement queries.
    • ShardAssignment(TenantId) for routing lookups; ShardAssignment(SiloId, IsPrimary) for capacity tallies.
  • Consistency
    • Use transactional updates when flipping primary during migration: write new primary, swap flags atomically, update RoutingTable, then emit tenant.placement.changed.

Integration touchpoints

  • Tenant: on activation, call AssignTenant with RegionResidency.RegionCode; store SiloId in routing projection (not in the Tenant aggregate).
  • Provisioning: consumes RoutingTable to know where to create resources; emits resource.binding.* which can update EndpointSet if endpoints are provisioned dynamically.
  • Observability: health checks update SiloState; failing silos trigger Draining and influence placement.
  • Notifications: send migration window notifications to Tenant Owner/Tech contacts.

Operational guidance

  • Rebalancing: when WarnAtTenants is exceeded or skew > threshold, compute candidate migrations within region; generate MigrationPlans for approval.
  • Disaster readiness: if FailoverRegionCodes are populated, keep cold/warm replicas and publish read-failover hints while ensuring legal compliance.
  • Cache discipline: RoutingTable is aggressively cached (edge/CDN) with short TTL and event-driven invalidation on tenant.placement.changed and endpoint.changed.

This context keeps policy (RegionPolicy), capacity (DataSilo), and placement (ShardAssignment) cleanly separated, while producing a crisp RoutingTable projection that every service can trust—fast to read, simple to cache, and governed by explicit, auditable events.


Secrets & Provider Credentials Context

Relationships

  • SecretBundle HasMany SecretVersion A bundle is the logical container for one secret across time (versions).
  • ProviderCredential Reference SecretBundle Credentials for external providers (email/SMS/payments/etc.) point to the bundle that holds their current/next secrets.
  • RotationPolicy Owned by SecretBundle or ProviderCredential Policies travel with what they govern to keep rotation SLOs enforceable.

Types in this context

  • Aggregates: ISecretBundle, IProviderCredential, IRotationPolicy
  • Entities/VOs: ISecretVersion (entity), Grant (VO), SecretRef (VO), ScopeRef (VO)
  • Enums: SecretScope, SecretVersionState, GrantPermission, CredentialStatus

Secrets are never stored in cleartext here. Only metadata, KMS pointers, hashes/fingerprints, and access grants live in the write model. Retrieval returns a short-lived materialization or a vault URI after authn/z.


ISecretBundle — properties (contract + semantics)

Property Type Description & rules
SecretBundleId Guid Aggregate id.
Key string Canonical slug (e.g., smtp.sendgrid.api_key). Unique per (Environment,Region,Scope).
Environment string prod | staging | dev | … for separation of duties.
Region string? Optional regionalization (EU, US, …).
Scope SecretScope Where this secret applies (Global, Region, Tenant, Provider, Connector, Service).
OwnerRef ScopeRef Who owns it (e.g., { type:"Service", id:"notifications-svc" } or { type:"Tenant", id:tenantId }).
ActiveVersionId Guid? The version currently in use. Only one version can be active.
NextVersionId Guid? Optional pre-staged version for zero-downtime rotation.
RotationPolicy IRotationPolicy Min/max age, overlap/grace, verification hooks.
Grants IReadOnlyList<Grant> Who can use/rotate/read metadata (least privilege).
CreatedAtUtc / UpdatedAtUtc DateTime Lifecycle timestamps.
Tags IReadOnlyList<KeyValueTag> Metadata (do not include secrets).

ISecretVersion — properties (child entity)

Property Type Description & rules
SecretVersionId Guid Identity for audit/activation.
BundleId Guid Parent link.
State SecretVersionState (Staged, Active, Revoked, Compromised, Superseded) State machine; only one Active.
KmsKeyId string KMS key alias/id used for envelope encryption.
CiphertextRef string Vault URI or storage pointer (e.g., vault://…/versions/…). No cleartext.
Fingerprint string Hash (HMAC/KEK) of the plaintext for integrity checks (non-reversible).
CreatedAtUtc DateTime When version was created.
ActivatedAtUtc? / RevokedAtUtc? DateTime? Lifecycle anchors.
ExpiresAtUtc? DateTime? Optional expiry; rotation jobs act before this.

IProviderCredential — properties (aggregate)

Property Type Description & rules
ProviderCredentialId Guid Aggregate id.
Provider string Slug (sendgrid, twilio, stripe, smtp, …).
AccountId string Provider account/project id.
Environment string prod | staging | ….
Region? string? Optional region if provider keys are regional.
Status CredentialStatus (Active, Disabled, Rotating, Revoked) Operational state.
SecretBundle ISecretBundle Reference to the bundle that contains the secret material.
AllowedActions IReadOnlyList<string> Provider-specific scopes (e.g., email.send, sms.send, payments.charge).
LastRotatedAtUtc? / NextRotationDueAtUtc? DateTime? For SLO tracking.
Tags IReadOnlyList<KeyValueTag> Metadata for ops (team, system, purpose).

IRotationPolicy — properties (aggregate or VO)

Property Type Description & rules
MinAgeDays int Earliest allowed rotation (to avoid thrash).
MaxAgeDays int SLA for rotation; schedulers alert/rotate before this.
OverlapMinutes int Window where current and next are both valid to allow smooth cutover.
VerificationWebhook? Uri? Optional endpoint to verify new version before promotion.
ActivationStrategy string (dual-write, flip-pointer, rollout-traffic) How consumers transition.
GraceOnRevokeMinutes int Grace period for consumers to refresh before hard revoke.

Grant (VO)

Property Type Description & rules
SubjectType string (Service, Worker, User, Job) Who can act.
SubjectId string Identity of subject (e.g., workload identity, group id).
Permissions IReadOnlyList<GrantPermission> Use, Rotate, ReadMetadata. Never ReadPlaintext.
Scope ScopeRef Where permission applies (tenant/region/service).
ExpiresAtUtc? DateTime? Optional temporal bound.

SecretRef (VO) & ScopeRef (VO)

VO Fields Notes
SecretRef { bundleKey, versionId? } Pointer used by consumers. Usually returned as vault://bundleKey#versionId?.
ScopeRef { type, id } Identifies owner/tenant/service; used in grants and audit.

Enums

Enum Members
SecretScope Global, Region, Environment, Tenant, Provider, Connector, Service
SecretVersionState Staged, Active, Revoked, Compromised, Superseded
GrantPermission Use, Rotate, ReadMetadata
CredentialStatus Active, Disabled, Rotating, Revoked

Invariants & policies

  • No plaintext at rest: only CiphertextRef and non-reversible Fingerprint are stored; logs/events never include secret material.
  • Single active version: exactly one Active version per bundle. NextVersionId may exist for overlap, but not marked Active simultaneously.
  • Rotation SLO: if ActiveVersion age ≥ MaxAgeDays (policy), rotation must be initiated; schedulers raise alerts prior to breach.
  • Least privilege: grants must use the narrowest feasible Scope and minimal Permissions. Rotation requires Rotate; consumers only have Use.
  • Tenant isolation: bundles with Scope=Tenant can only be granted to subjects scoped to that tenant.
  • Promotion gate: to promote a Staged version to Active, verification must pass (optional webhook/health checks) within OverlapMinutes.
  • Revocation window: after Revoked, consumers have at most GraceOnRevokeMinutes to refresh before access is denied.

Domain events

  • secret.created{ bundleId, key, environment, region?, scope, activeVersionId? }
  • secret.rotated{ bundleId, previousVersionId?, newVersionId, method, overlappedForMinutes }
  • secret.revoked{ bundleId, versionId, reason }
  • provider.credential.bound{ credentialId, provider, accountId, bundleKey }
  • provider.credential.updated{ credentialId, status, nextRotationDueAtUtc? }

(Events contain identifiers, not plaintext; correlation via bundleId/credentialId, partitioned by tenantId when scope is tenant.)


Retrieval & rotation flows

Retrieve (for a service):

  1. Service authenticates (workload identity) → GetAccessToken(subjectId, scopeRef).
  2. Calls GetSecret(ref) → gets either a vault URI it can dereference or a short-lived materialization (TTL seconds).
  3. Audit record: subject, bundleKey, versionId, scopeRef, traceId.

Rotate (operator or job):

  1. PutVersion(bundleKey, ciphertextRef, fingerprint) (state=Staged).
  2. Optional verification hook (smoke send, sandbox charge…).
  3. PromoteVersion(bundleKey, versionId) → flips ActiveVersionId (dual valid if overlap configured).
  4. Notify dependents (change events or out-of-band secrets manager signals).
  5. RevokeVersion(bundleKey, previousVersionId) after overlap/grace window.

Zero-downtime rotation for providers often uses dual credentials: current+next both authorized at the provider for the overlap window.


APIs / projections

Commands

  • CreateSecretBundle(key, environment, region?, scope, ownerRef, rotationPolicy, tags?)
  • PutSecretVersion(bundleKey, ciphertextRef, fingerprint) → returns versionId
  • PromoteSecretVersion(bundleKey, versionId)
  • RevokeSecretVersion(bundleKey, versionId, reason)
  • IssueGrant(bundleKey, grant) / RevokeGrant(bundleKey, subjectId)
  • CreateProviderCredential(provider, accountId, environment, region?, bundleKey, allowedActions, tags?)
  • UpdateCredentialStatus(credentialId, status) / ScheduleRotation(credentialId, when)

Reads / Projections

  • CredentialCatalog (redacted):

{ credentialId, provider, accountId, environment, region?, status,
  bundleKey, lastRotatedAtUtc?, nextRotationDueAtUtc?, allowedActions[], tags[] }
* SecretBundleView: bundle metadata, no ciphertexts. * RotationQueue: bundles/credentials due for rotation (SLO-aware).


ORM notes

  • Aggregates
    • SecretBundle root with owned RotationPolicy and Grants; SecretVersion child table keyed by SecretVersionId (FK BundleId).
    • ProviderCredential root referencing SecretBundle by key/id (no navigation to secret material).
  • Uniqueness & indexes
    • Unique (Key, Environment, Region, Scope) on bundles.
    • Unique filtered index SecretVersion(BundleId, State WHERE State='Active').
    • Index ProviderCredential(provider, environment, region, status).
  • Hard deletes: avoid. Use Revoked/Disabled states; retain history for audit.
  • Encryption: envelope encryption via KMS; rotate KEKs per policy; store only KmsKeyId and CiphertextRef.

Integration touchpoints

  • Provisioning: steps carry secret references; engine resolves at runtime with a short-lived token from this context.
  • Notifications/Billing/Integrations: their provider credentials are bound here; rotation is coordinated centrally (downstream services reload).
  • Audit: every retrieve/rotate/revoke produces an immutable audit record correlated by traceId.
  • Access Rules: evaluation ensures caller has a Grant with Use or Rotate for the specific ScopeRef.

Operator guidance

  • Prefer service-to-service identities over long-lived API keys. Where unavoidable, enforce aggressive rotation and overlap with dual credentials.
  • Maintain runbooks for compromised credentials: mark Compromised, generate new version, immediate revoke after overlap 0, notify incident mgmt.
  • Periodically validate Fingerprint against live material to detect drift between vault and consumers.

This context gives you a single source of truth for secret metadata and provider credentials, with clear rotation SLOs, least-privilege grants, event-driven updates, and zero-downtime cutovers—without ever leaking secret material into your domain model, events, or logs.


Observability & Incident Management Context

Relationships

  • AlertRule HasMany Incidents An alert rule can generate many incidents over time (deduped by correlation keys).
  • Incident Reference SLA (by key+version) Each incident optionally links to the SLA package in effect for the affected tenants/editions at the time of breach (to drive credits).
  • MaintenanceWindow affects AlertRule & Incident evaluations Active maintenance can silence/suppress alerts and pause SLA clocks.

Types in this context

  • Aggregates: IAlertRule, IIncident, IMaintenanceWindow
  • Entities/VOs: SloTarget (VO), RunbookRef (VO), OwnerRef (VO), ScopeRef (VO)
  • Enums: Severity, IncidentStatus, AlertState, DetectionSource, SlaClockState

All cross-context references (Tenant, Region, Edition, SLA) are IDs/keys only. No navigations.


IAlertRule — properties (contract + semantics)

Property Type Description & rules
AlertRuleId Guid Aggregate id.
Key string Canonical slug (e.g., api-latency-p99). Unique per environment.
DisplayName string Human-friendly title.
Description string What’s being monitored and why.
Severity Severity (P1,P2,P3,P4) Default severity for incidents opened by this rule.
Slo SloTarget (VO) Target objective (SLO) tied to this alert (e.g., latency_p99 ≤ 300ms over 99.9% req).
Query string Metric/log query or detector expression (PromQL/SQL/CEL-like).
Threshold decimal Numeric threshold (or use Query to embed it).
EvaluationWindow TimeSpan Window over which to evaluate (e.g., 5m).
ForDuration TimeSpan Breach must persist for this long before firing (debounce).
EvaluateEvery TimeSpan Evaluation cadence (e.g., 1m).
Scope ScopeRef (VO) Where this rule applies (Region/Silo/Service/Tenant/Product/Edition).
SilencedUntilUtc? DateTime? Temporary silence (e.g., during maintenance).
Runbook RunbookRef (VO) Primary remediation instructions.
Labels IDictionary<string,string> Arbitrary labels (team, component, env).
State AlertState (Enabled,Disabled) Operational toggle.
Owner OwnerRef (VO) Owning team/service for on-call routing.
LastEvaluatedAtUtc? DateTime? Last evaluation timestamp.
CreatedAtUtc / UpdatedAtUtc DateTime Lifecycle timestamps.

SloTarget (VO)

Property Type Description
Indicator string SLI key (e.g., availability, latency_p99, error_rate).
Target decimal Target value (e.g., 99.9 for availability, 300 for ms).
Operator string (>=,<=) Comparison direction.
Measurement string How measured (e.g., “HTTP 2xx/total”, “p99 over 5m”).

IIncident — properties (contract + semantics)

Property Type Description & rules
IncidentId Guid Aggregate id.
Key string Correlation key (e.g., eu:api-latency-p99); dedupes repeat fires.
Title string Short human-readable summary.
Description string Detailed context/symptoms.
Status IncidentStatus (Open,Acknowledged,Mitigated,Resolved,Canceled) Lifecycle; “Mitigated” = user impact addressed while root cause ongoing.
Severity Severity Initial severity; may be reclassified.
DetectionSource DetectionSource (Alert,Manual,External) Who/what opened it.
AlertRuleId? Guid? When opened by an alert.
TenantIds IReadOnlyList<Guid> Affected tenants (non-empty for tenant-impacting incidents).
RegionCodes IReadOnlyList<string> Impacted regions.
SiloIds IReadOnlyList<string> Impacted silos/clusters.
ProductKeys / EditionKeys IReadOnlyList<string> Impacted catalog surfaces (optional).
SlaRef? { key:string, version:string }? SLA reference for credits assessment.
SlaClock SlaClockState (Running,Paused,Stopped) Whether SLA timers are counting (paused during approved maintenance).
OpenedAtUtc DateTime When created.
AcknowledgedAtUtc? DateTime? When an on-call acked.
MitigatedAtUtc? DateTime? When user-facing impact mitigated.
ResolvedAtUtc? DateTime? When fully resolved.
Owner OwnerRef (VO) Current incident commander/team.
Runbooks IReadOnlyList<RunbookRef> Contextual remediation docs.
RelatedTraces IReadOnlyList<string> Trace/session IDs for root cause analysis.
RelatedTickets IReadOnlyList<string> Links to Jira/ServiceNow/etc.
Updates IList<IncidentUpdate> Timeline (see below).
Tags IDictionary<string,string> Labels (component, customer-tier).

IncidentUpdate (child entity) { WhenUtc: DateTime, Author: string, Message: string, Visibility: string("internal"|"public"), Attachments?: string[] }


IMaintenanceWindow — properties

Property Type Description & rules
MaintenanceWindowId Guid Aggregate id.
Title string Purpose (“EU DB patching”).
Scope ScopeRef (VO) Region/Silo/Service/Tenant scope.
StartsAtUtc / EndsAtUtc DateTime Window (Ends exclusive).
ApprovedBy string Approver (user/service).
SilenceAlerts bool Whether to auto-silence rules in scope.
PauseSlaClocks bool Whether incidents’ SLA clocks pause in scope.
Status string (Scheduled,Active,Completed,Canceled) Lifecycle.
CreatedAtUtc / UpdatedAtUtc DateTime Timestamps.

RunbookRef (VO) & OwnerRef (VO) & ScopeRef (VO)

VO Fields Notes
RunbookRef { key:string, url:string } Operational doc pointer.
OwnerRef { team:string, onCallRotation?:string } Owning team & rotation id.
ScopeRef { kind:"Region" | "Silo" | "Service" | "Tenant" | "Product" | "Edition", id:string } Where rule/window/incident applies.

Enums

Enum Members
Severity P1,P2,P3,P4 (P1 = highest user impact)
IncidentStatus Open,Acknowledged,Mitigated,Resolved,Canceled
AlertState Enabled,Disabled
DetectionSource Alert,Manual,External
SlaClockState Running,Paused,Stopped

Invariants & policies

  • Incident scoping Every incident must reference at least one Region or Tenant (or both). For tenant-impacting incidents, TenantIds must not be empty.
  • SLA breach workflow When an incident violates SloTarget for an Edition/SLA in scope, emit incident.sla_breach and start credits.assess in Billing with the affected tenant set and breach window.
  • Ack & escalation P1 incidents must be Acknowledged within, e.g., 5 minutes; auto-escalate owner rotation if breached.
  • Maintenance behavior Active MaintenanceWindow with PauseSlaClocks=true pauses the SlaClock for matching incidents; with SilenceAlerts=true the system suppresses or auto-silences alert fires in scope.
  • Dedup & correlation New alerts create or attach to an open incident with the same Key (matching rule+scope). Reopen only if last resolution was within a cool-off window (configurable).

Domain events

  • alert.fired{ alertRuleId, key, severity, scope, evaluationWindow, forDuration, value, threshold, time }
  • alert.resolved{ alertRuleId, key, time }
  • incident.opened{ incidentId, key, title, severity, scope, openedAtUtc, detectionSource, tenantIds?, regionCodes? }
  • incident.acknowledged{ incidentId, acknowledgedAtUtc, owner }
  • incident.mitigated{ incidentId, mitigatedAtUtc }
  • incident.resolved{ incidentId, resolvedAtUtc, rootCause?, followUps? }
  • incident.sla_breach{ incidentId, sla:{key,version}, tenants:[…], fromUtc, toUtc, slo:{indicator,target,operator,measurement} }
  • maintenance.started|completed|canceled{ maintenanceWindowId, scope, startsAtUtc, endsAtUtc }

Events carry IDs/keys and scope; no PII beyond tenant IDs. Correlate via incidentId and alertRuleId, plus traceId/spanId in headers.


APIs / projections

Commands

  • Alert rules: CreateRule, UpdateRule, EnableRule, DisableRule, SilenceRuleUntil
  • Incidents: OpenIncident, AcknowledgeIncident, MitigateIncident, ResolveIncident, ReclassifySeverity, AddIncidentUpdate
  • Maintenance: ScheduleMaintenance, ActivateMaintenance, CompleteMaintenance, CancelMaintenance

Reads / Projections

  • IncidentTimelineView

{
  incidentId, key, title, status, severity, detectionSource,
  tenants:[...], regions:[...], siloIds:[...],
  slaRef?, slaClock, openedAtUtc, acknowledgedAtUtc?, mitigatedAtUtc?, resolvedAtUtc?,
  updates:[{ whenUtc, author, message, visibility }],
  relatedTraces:[...], relatedTickets:[...]
}
* StatusPageView (public/tenant-scoped): incidents affecting a region/tenant with filtered updates. * AlertRuleCatalog: rules by scope, severity, owner. * SlaBreachReport: incidents with breach windows and affected tenants (feeds Billing).


Evaluation engine (operational notes)

  • Scheduler evaluates enabled rules every EvaluateEvery, fetching metrics via adapters (Prometheus, CloudWatch, etc.); applies EvaluationWindow and ForDuration.
  • Silence/maintenance is checked prior to opening/attaching incidents.
  • Correlation: hash (alertRuleId, scope) to form Incident.Key; attach subsequent fires until resolved.
  • Escalation uses OwnerRef.onCallRotation to page/notify.
  • SLA breach detector computes SLI against SloTarget over the incident window; if target missed, emit incident.sla_breach.

ORM notes

  • Aggregates
    • AlertRule root with owned SloTarget, ScopeRef, RunbookRef, OwnerRef, and label dictionary (JSON).
    • Incident root with child IncidentUpdate table; arrays for scope fields stored as join tables or JSON (depending on DB).
    • MaintenanceWindow root with owned ScopeRef.
  • Indexes
    • AlertRule(State, Scope.kind, Scope.id) for evaluation selection.
    • Incident(Status, Severity, OpenedAtUtc DESC) for on-call dashboards.
    • GIN/JSON indexes on labels/scope fields if using JSON.
  • Constraints
    • Check EndsAtUtc > StartsAtUtc on maintenance; AcknowledgedAtUtc ≥ OpenedAtUtc on incidents.
    • Foreign keys only inside the context; external references are keys/ids without FKs.

Integration touchpoints

  • Audit & Compliance: emit audit records for rule changes and every incident state transition (with traceId).
  • Billing: consume incident.sla_breach to assess service credits for affected tenants within the breach window.
  • Notifications: send pages/emails/SMS based on severity and owner; publish sanitized StatusPageView updates.
  • Data Platform: export incidents/alerts for analytics; compute MTTA/MTTR and SLO performance reports.

Operator guidance

  • Keep rule runbooks current and link to automated fix scripts when possible.
  • Regularly review false positives/negatives to tune thresholds and ForDuration.
  • Enforce post-incident reviews (P1/P2) with action items tracked in RelatedTickets.
  • Ensure maintenance windows are scheduled with sufficient notice and have PauseSlaClocks configured when appropriate.

This context makes reliability first-class: clear alerting semantics tied to SLOs, rich incident timelines with tenant/region scoping, and a direct path from validated SLA breaches to Billing credits—all without crossing DDD boundaries.


Search & Discovery Context

Relationships

  • IndexDefinition HasMany IngestPipelines Each index is fed by one or more pipelines (e.g., Catalog events, Tenant updates, Docs).
  • IndexDefinition HasMany IndexShards For capacity and regional isolation; shard metadata lives in this context (search engine keeps its own state too).
  • IndexDefinition References SynonymSets (by locale/key) Indexes can attach multiple synonym lists per language/locale.

Types in this context

  • Aggregates: IIndexDefinition, IIngestPipeline, ISynonymSet
  • Entities/VOs: IIndexShard (entity), AnalyzerSpec (VO), FieldMapping (VO), ScopeRef (VO)
  • Enums: IndexState, PipelineState, ShardState

Cross-context data is copied into the index via pipelines; never navigated live. All tenant-scoped documents carry tenantId and are RLS-enforced at query time.


IIndexDefinition — properties (contract + semantics)

Property Type Description & rules
IndexId Guid Aggregate id.
Key string Canonical slug (e.g., catalog.search.v1). Unique per environment/region.
DisplayName string Human label.
Description string Purpose & scope of content (e.g., “Products/Edt/Features and docs”).
Scope ScopeRef (VO) Global or Region; some indexes are region-local for residency.
Analyzer AnalyzerSpec (VO) Tokenization, stemming, stopwords, language(s).
FieldMappings IReadOnlyList<FieldMapping> Schema: field name, type (text, keyword, numeric, date, vector), stored/indexed flags.
SynonymSetKeys IReadOnlyList<string> Attached synonym sets by key/locale.
RefreshInterval TimeSpan Target refresh for near-real-time search.
ShardCount int Planned primary shards.
ReplicaCount int Replicas per shard.
State IndexState (Draft,Built,Active,ReadOnly,Deleting,Deleted) Lifecycle state.
CreatedAtUtc / UpdatedAtUtc DateTime Lifecycle timestamps.
Tags IReadOnlyList<KeyValueTag> Metadata (team, dataset).

FieldMapping (VO) { Name:string, Type:string, Indexed:bool, Stored:bool, Searchable:bool, Aggregatable:bool, AnalyzerKey?:string, VectorDims?:int, Pii?:bool, TenantScoped?:bool }


AnalyzerSpec (VO)

Property Type Description
Key string Analyzer identifier (e.g., standard-en, icu-multi).
Languages IReadOnlyList<string> ISO codes (en, de, …).
Stopwords? IReadOnlyList<string>? Optional per-language stopwords.
Stemming string? Algorithm or none.
SynonymMode string (off, expand, replace) How synonyms apply.

IIngestPipeline — properties (aggregate)

Property Type Description & rules
IngestPipelineId Guid Aggregate id.
IndexKey string Target index by key (no navigation).
Key string Pipeline slug (e.g., catalog-events-v2).
DisplayName string Human label.
State PipelineState (Disabled,Enabled,Draining) Operational state.
Source string Source type (event:catalog, db:readmodel, blob:docs).
Filters IDictionary<string,string> Event types, regions, tenants (for backfills).
Transforms IReadOnlyList<string> Ordered transform keys (pii.scrub, text.normalize, vector.embed:all-MiniLM).
Mappings IReadOnlyList<string> Mapping templates to target fields (e.g., catalog.product → product.*).
DlqRef? string? Dead-letter queue reference.
BatchSize int Indexing batch size.
MaxInFlight int Parallelism.
CreatedAtUtc / UpdatedAtUtc DateTime Timestamps.

ISynonymSet — properties (aggregate)

Property Type Description & rules
SynonymSetId Guid Aggregate id.
Key string Slug (catalog-en, acronyms-global).
Locale string BCP-47 (en-US, de-DE, und for locale-agnostic).
Entries IReadOnlyList<string> Lines like "sso, single sign-on, saml".
Status string (Draft,Active,Deprecated) Lifecycle.
UpdatedAtUtc DateTime Timestamp for cache busting.

IIndexShard — properties (entity)

Property Type Description & rules
ShardId string Engine shard id.
RegionCode string Region where this shard resides.
PrimaryNode string Node id.
ReplicaNodes IReadOnlyList<string> Replica node ids.
State ShardState (Active,Initializing,Relocating,ReadOnly) Operational state.
DocCount long Approx document count.
StoreGb double Store size.
UpdatedAtUtc DateTime Last sync time from engine.

Document shapes (examples)

  • Catalog document

{
  type: "catalog",
  productKey, productDisplayName,
  editionKey?, featureKey?,
  status, tags[],
  tenantId?: null,         // catalog is global
  text: "...",             // concatenated + normalized fields
  facets: { product, edition, feature, status },
  updatedAtUtc
}
* Tenant document

{
  type: "tenant",
  tenantId, displayName, domain, regionCode,
  editionKey?, features[], tags[],
  text: "...",
  facets: { region:"EU", edition:"enterprise" },
  updatedAtUtc
}

All tenant-scoped docs must include tenantId and are filtered by RLS at query time.


Events

  • index.built{ indexKey, shardCount, replicaCount, analyzerKey }
  • index.updated{ indexKey, changes: { mappings?, analyzer?, synonyms? } }
  • index.deleted{ indexKey }
  • document.indexed{ indexKey, docType, docId, tenantId?, updatedAtUtc }
  • document.reindexed{ indexKey, docType, range|criteria, count }

(Events are additive, idempotent; no document bodies are emitted.)


Invariants & policies

  • PII minimization Fields marked Pii=true in FieldMappings are either excluded, hashed, or tokenized without storing raw values. Default = deny unless explicitly allowlisted.
  • Tenant isolation Tenant docs require tenantId. Queries must supply tenantId or a privileged scope; engine-side filters enforce RLS.
  • Residency compliance Region-scoped indexes store/serve only from that region; cross-region queries are disabled unless explicitly allowed. Synonym sets can be global but cached per region.
  • Schema evolution Additive changes permitted; destructive changes require a reindex plan (build new index, swap alias).
  • Synonym sanity Reject cycles/degenerate synonym rules (e.g., a ↔ a); limit expansion fan-out to prevent query explosion.
  • Capacity ShardCount × (ReplicaCount+1) within engine cluster limits; warn when DocCount/Shard exceeds thresholds.

APIs / projections

Commands

  • CreateIndex(key, analyzer, fieldMappings, shardCount, replicaCount, refreshInterval, scope)
  • UpdateIndex(key, analyzer?, fieldMappings?, synonymSetKeys?)
  • DeleteIndex(key) (transitions to ReadOnly then Deleting)
  • CreatePipeline(key, indexKey, source, transforms, mappings, batchSize, maxInFlight)
  • EnablePipeline(pipelineId) / DisablePipeline(pipelineId) / DrainPipeline(pipelineId)
  • PutSynonymSet(key, locale, entries[], status)
  • Reindex(indexKey, fromAlias?, where?) (build-new & swap through alias)

Reads

  • Search(query, tenantId, filters?, page?, pageSize?, sort?, vector? ) Returns { total, hits:[{ id, score, highlights, facets, … }] } (RLS by tenantId).
  • Suggest(prefix, tenantId, limit) — typeahead.
  • SearchFacetView(indexKey) — aggregated facets for UIs (e.g., products/editions/features/status).
  • IndexCatalog — definitions, pipelines, shard health.
  • PipelineLagView(pipelineId) — event offsets, DLQ size.

Ranking & retrieval

  • Keyword: BM25 with field boosts (title > displayName > text).
  • Semantic: optional dual retrieval using vector fields (VectorDims present) and ANN search; hybrid rank: α·BM25 + (1−α)·Cosine.
  • Faceting: use Aggregatable=true fields (productKey, editionKey, status, regionCode) for counts.
  • Highlighting: pre/post tags applied to text snippets; strip PII.

Pipelines (operational notes)

  • Sources: catalog.* and tenant.* events, plus batched backfills from read models.
  • Transforms: normalize text (lowercase, ASCII fold), apply synonyms, scrub PII, derive composite text, compute embeddings (optional), map to fields.
  • Failures: validation errors → DLQ; transient errors → retry with backoff.
  • Idempotency: document _id constructed from natural key (e.g., catalog:product:{productKey} or tenant:{tenantId}).

ORM & engine notes

  • Meta store (relational): IndexDefinition, IngestPipeline, SynonymSet, IndexShard (read-through).
  • Engine: Elastic/OpenSearch/Equiv keeps documents and shard state. We mirror minimal shard stats for ops dashboards.
  • Aliases: read alias key@read, write alias key@write to support zero-downtime reindex.
  • Indexes & constraints (meta store): unique (Key, Scope.Region?); FK-less references by key for loose coupling.

Integration touchpoints

  • Catalog: consumes product/edition/feature events; transforms to Catalog docs for discovery.
  • Tenants: consumes tenant.* (created/activated/suspended) to index tenant directory (RLS).
  • Observability: exposes pipeline lag and shard health to alerting.
  • Notifications/Portal: uses SearchFacetView for filters and typeahead across products/features and tenant resources.

Example: SearchFacetView (Catalog)

{
  "indexKey": "catalog.search.v1",
  "facets": {
    "productKey": [{"key":"crm-suite","count":124},{"key":"ai-doc-intel","count":52}],
    "editionKey": [{"key":"standard","count":98},{"key":"enterprise","count":78}],
    "featureKey": [{"key":"api.core","count":65},{"key":"webhooks.outbound","count":44}],
    "status": [{"key":"published","count":160},{"key":"deprecated","count":16}]
  },
  "updatedAtUtc": "2025-09-30T10:12:00Z"
}

Security & compliance

  • Enforce RLS with a mandatory tenantId filter for tenant-scoped indexes; admin queries require elevated scopes.
  • Maintain a PII field registry; pipelines must scrub or hash these before indexing.
  • Support right-to-erasure by tracking document sources/keys → targeted delete/reindex plans.

This context gives your platform a robust, compliant search layer: index schemas and analyzers as first-class domain objects, event-driven pipelines that copy data from authoritative contexts, strict tenant isolation, and flexible ranking (keyword + semantic) to power discovery and in-app search.


Data Platform & Reporting Context

Relationships

  • Dataset HasMany IngestJobs A dataset is produced by one or more jobs (batch/stream) that load or transform data into its storage.
  • Report Reference Dataset Each report or dashboard is defined against a single semantic dataset (can join others internally via the semantic layer), version-pinned for reproducibility.
  • DataContract Governs Dataset The contract specifies schema, semantics, and privacy/quality rules for a dataset version.

Types in this context

  • Aggregates: IDataset, IDataContract, IReport
  • Entities/VOs: IIngestJob (entity), SchemaVersion (VO), RetentionPolicy (VO), PartitionSpec (VO), LineageRef (VO), QualityRule (VO)
  • Enums (suggested): DatasetType, StorageFormat, JobKind, JobStatus, ContractStatus, ReportStatus, DataClassification

Authoritative data lives in storage engines (data lake/warehouse). This context governs metadata, contracts, versions, lineage, retention, and exposed read endpoints. All cross-context references are IDs/keys only.


IDataset — properties (contract + semantics)

Property Type Description & rules
DatasetId Guid Aggregate id.
Key string Canonical slug (e.g., catalog.products.v1, usage.events.daily). Unique per environment/region.
DisplayName string Human label for catalogs/BI.
Description string Purpose, grain, and primary keys (at a glance).
Type DatasetType (Raw, Curated, Mart, Semantic) Lifecycle stage in the lakehouse.
Storage { Catalog:string, Schema:string, Table:string, Format:StorageFormat } Physical mapping (warehouse/lake).
Partitioning PartitionSpec (VO) How data is partitioned (e.g., by ingestion_date, tenantId, region).
Contract IDataContract Governing contract (current version).
SchemaVersion SchemaVersion (VO) Version pin (SemVer). Breaking changes bump major.
Classification DataClassification (Public, Internal, Confidential, Restricted) Highest classification present.
Retention RetentionPolicy (VO) How long data is retained (legal/compliance aligned).
Lineage IReadOnlyList<LineageRef> Upstream sources (keys) and transforms.
TenantScoped bool If true, every row must include tenantId and be RLS-filterable.
AccessRoles IReadOnlyList<string> RBAC groups allowed to query. Additional ABAC checks may apply.
CreatedAtUtc / UpdatedAtUtc DateTime Lifecycle timestamps.
Tags IReadOnlyList<KeyValueTag> Metadata (domain, steward, SLA).

IDataContract — properties (aggregate)

Property Type Description & rules
DataContractId Guid Aggregate id.
Key string Contract slug (e.g., usage.events.contract).
Version SchemaVersion (VO) SemVer for the contract.
Status ContractStatus (Draft,Published,Deprecated) Lifecycle.
OwnerTeam string Stewardship team for approvals and change control.
Schema IReadOnlyList<{ Name:string, Type:string, Nullable:bool, Pii:bool, Description?:string }> Logical schema with PII flags.
PrimaryKey IReadOnlyList<string> Column set for row identity (immutable).
NaturalKey? IReadOnlyList<string>? Optional business key(s).
SCDMode string (None,SCD1,SCD2) Change handling strategy (for dims).
QualityRules IReadOnlyList<QualityRule> Declarative data quality checks (e.g., %null(tenantId)=0).
BreakingChangeNotes? string? Rationale when major version changes.
PublishedAtUtc? DateTime? Publication timestamp.

QualityRule (VO) { Key:string, Expression:string, Severity:"Error"|"Warn", Description?:string }


IReport — properties (aggregate)

Property Type Description & rules
ReportId Guid Aggregate id.
Key string Canonical slug (e.g., billing.mrr.trend).
DisplayName string Human label.
Description string Business purpose and target audience.
DatasetKey string Reference to the semantic dataset powering this report.
SchemaVersion SchemaVersion (VO) Version pin to ensure consistent visuals/queries.
Parameters IReadOnlyList<{ Name:string, Type:string, Required:bool, Default?:string }> Runtime parameters (e.g., tenantId, region, dateRange).
AccessRoles IReadOnlyList<string> Who may render/download.
Status ReportStatus (Draft,Published,Deprecated) Lifecycle.
CreatedAtUtc / UpdatedAtUtc DateTime Lifecycle timestamps.
Tags IReadOnlyList<KeyValueTag> Metadata (KPI, department).

IIngestJob — properties (entity)

Property Type Description & rules
IngestJobId Guid Identity for monitoring/audit.
DatasetKey string Target dataset (by key).
Kind JobKind (Batch,Stream,Backfill) Execution model.
Status JobStatus (Idle,Running,Succeeded,Failed,Degraded,Disabled) Current state.
Source { Type:string, Ref:string } e.g., event:metering, db:catalog, blob://….
Schedule? string? CRON or schedule definition for batch.
WatermarkColumn? string? For incremental loads (e.g., updated_at).
LastWatermark? string? Last processed value (ISO datetime/ULID).
LateArrivalWindow TimeSpan Period to accept late data (re-processing rules apply).
Retries { Max:int, Backoff:string } Retry policy.
OwnerTeam string On-call owner for failures.
LastRunAtUtc? / NextRunAtUtc? DateTime? Planner/monitoring.
DlqRef? string? Dead-letter sink for bad records.

Value objects

SchemaVersion (VO){ Major:int, Minor:int, Patch:int } (compare rules: any change to PrimaryKey/types → Major; additive columns → Minor; doc-only tweaks → Patch). RetentionPolicy (VO){ Mode:"Time"|"Rows"|"Hybrid", Duration?:TimeSpan, Rows?:long, LegalHold?:bool } PartitionSpec (VO){ Keys:string[], Strategy:"Range"|"Hash"|"Composite" } LineageRef (VO){ UpstreamKey:string, Transform:string, Columns?:string[] }


Enums

Enum Members
DatasetType Raw, Curated, Mart, Semantic
StorageFormat Parquet, Iceberg, Delta, BigQuery, View
JobKind Batch, Stream, Backfill
JobStatus Idle, Running, Succeeded, Failed, Degraded, Disabled
ContractStatus Draft, Published, Deprecated
ReportStatus Draft, Published, Deprecated
DataClassification Public, Internal, Confidential, Restricted

Invariants & policies

  • Contracts govern schemas Breaking schema changes (type/primary key changes, column removal/rename) require SchemaVersion.Major++ and a new dataset key or table with dual-write/swap.
  • Retention enforcement Apply RetentionPolicy at storage (e.g., Iceberg/Delta VACUUM) and at query endpoints; align with Audit/Compliance retention.
  • Tenant isolation If TenantScoped=true, the dataset MUST include a tenantId field, and endpoints must enforce RLS (filter by caller’s tenant).
  • PII minimization Columns marked Pii=true require hashing/tokenization at Raw or removal from Curated/Mart unless explicitly allowed by policy.
  • Idempotent ingest Batch jobs use Watermark + upsert/merge keyed by PrimaryKey. Stream jobs dedupe by event id to achieve at-least-once → exactly-once semantics.
  • Late data Records within LateArrivalWindow trigger re-aggregation/re-merging; outside the window go to DLQ for manual handling.
  • Reproducibility Reports pin SchemaVersion; rendering must fail fast if the dataset contract advances a major.

Domain events

  • dataset.published{ datasetKey, schemaVersion, type, classification, retention }
  • dataset.schema.changed{ datasetKey, from:version, to:version, breaking:boolean, notes }
  • dataset.deprecated{ datasetKey, sunsetAfterUtc }
  • report.published{ reportKey, datasetKey, schemaVersion, parameters[] }
  • report.deprecated{ reportKey }
  • ingest.job.started|succeeded|failed{ jobId, datasetKey, kind, watermark?, counts, error? }

Events are minimal; payloads avoid large schemas, referring to keys/versions instead.


APIs / projections

Read endpoints

  • SQL/Presto/Trino gateway (read-only): enforces RLS; exposes views {env}.{domain}_{datasetKey}_v{major} with access control.
  • Semantic API: Query(reportKey, parameters) returns typed frames for BI and programmatic use.
  • Metrics API: job health, lag, DLQ counts.

Commands

  • PublishDataset(datasetKey, contractKey@version, storage, partitioning, retention, tenantScoped, tags)
  • UpdateContract(contractKey, nextVersion, schema diff, qualityRules) (validations auto-derive breaking)
  • RegisterIngestJob(datasetKey, source, kind, schedule?, watermark?, retries, lateWindow, ownerTeam)
  • SetDatasetClassification(datasetKey, classification)
  • PublishReport(reportKey, datasetKey, schemaVersion, parameters, accessRoles)

Projections

  • DatasetCatalog

{ datasetKey, displayName, type, schemaVersion, storage:{catalog,schema,table,format},
  classification, retention, tenantScoped, ownerTeam, tags }
* JobRunView(jobId) — last N runs, status, watermarks, row counts, errors. * ReportCatalog — reports by dataset, with parameter metadata.


ORM & storage notes (metadata store)

  • Aggregates
    • Dataset root references DataContract by key/version; owns PartitionSpec, RetentionPolicy.
    • DataContract root owns schema columns and quality rules (child rows or JSON).
    • Report root pins dataset key and schema version.
  • Indexes
    • Unique on Dataset(Key) and (DataContract.Key, SchemaVersion); filtered unique on Report(Key).
    • Secondary indexes on Dataset(Type, Classification), Job(DatasetKey, Status).
  • Change control
    • Contract updates require owner approval; publishing emits dataset.published.
    • Migrations (major bumps) create new physical tables or versions and optionally backfill.

Lineage & quality

  • Capture upstream LineageRef on datasets (e.g., metering.eventsbilling.usage_daily).
  • Quality rules run in jobs; failures fail the job (for Error) or log/warn (for Warn) and push to DLQ.
  • Expose Data Lineage View for traceability (edges: contract key/version → dataset key → report key).

Security & compliance

  • Enforce RLS for tenant datasets; only internal roles may query cross-tenant aggregated marts, with anonymization where required.
  • Mask or tokenize PII at rest; audit all query access to Restricted datasets.
  • Support right-to-erasure workflows: locate rows by tenant/user key and delete across Raw/Curated/Mart with proofs.

Example: version bump workflow

  1. Propose schema change → UpdateContract (creates v2.0.0, marks breaking=true).
  2. Create datasetKey_v2 (or same key with SchemaVersion.Major=2 and new physical table).
  3. Dual-write ingest (v1 & v2) until confidence threshold.
  4. Republish dependent Reports pinned to v2.
  5. Deprecate v1 with sunsetAfterUtc, then archive per RetentionPolicy.

This context provides the governance backbone for analytical data: explicit contracts and versions, idempotent ingest with late data handling, strict retention and classification, and version-pinned reports—so your platform can deliver trustworthy metrics and dashboards without coupling execution to any single storage engine.


Marketplace & Integrations Context

Relationships

  • IntegrationApp HasMany Connectors One App (a vendor offering) can expose multiple Connectors (e.g., “CRM Sync”, “Slack Notifier”).
  • Installation Reference Tenant + Connector An Installation is the tenant-scoped binding of a Connector, including auth state and config. Uniqueness: (tenantId, connectorId).
  • Connector References OAuthClient + WebhookConfig Connector may require OAuth and/or webhooks; secrets live in Secrets & Provider Credentials via references.

Types in this context

  • Aggregates: IIntegrationApp, IConnector, IInstallation
  • Entities/VOs: IOAuthClient (entity), PermissionGrant (VO), WebhookConfig (VO), EndpointRef (VO)
  • Enums (suggested): AppStatus, ConnectorStatus, InstallationStatus, ConnectorAuthType, WebhookSignatureAlgorithm, GrantEffect

Cross-context refs to Tenant and Access enumerations use IDs/keys only. All sensitive material (client secrets, webhook signing keys) is stored indirectly via SecretBundle refs.


IIntegrationApp — properties (contract)

Property Type Description & rules
IntegrationAppId Guid Aggregate id.
Key string Canonical slug (acme.crm). Unique per environment.
DisplayName string Marketplace card title.
Description string What the app does, data flow summary (ingress/egress).
VendorName string Publisher shown in marketplace.
HomepageUrl Uri? Vendor site.
PrivacyUrl Uri? Policy link (required for publish).
SupportUrl Uri? Support link.
LogoAssetRef string? Asset id from Content/Branding.
Categories IReadOnlyList<string> e.g., “CRM”, “Messaging”.
RequiredScopes IReadOnlyList<PermissionGrant> Must map to AccessScope/Type; least privilege.
Status AppStatus (Draft,Published,Deprecated,Retired) Lifecycle; publish allowed only with policy checks.
CreatedAtUtc / UpdatedAtUtc DateTime Timestamps.
Tags IReadOnlyList<KeyValueTag> Search facets.

IConnector — properties (aggregate)

Property Type Description & rules
ConnectorId Guid Aggregate id.
AppKey string Reference to parent App by key.
Key string Connector slug (crm.sync, slack.notify).
DisplayName string Human label.
Description string Feature description & required permissions.
Status ConnectorStatus (Draft,Published,Deprecated) Lifecycle.
AuthType ConnectorAuthType (OAuth2,APIKey,None) Required authorization to bind tenant.
OAuthClient? IOAuthClient? Present when AuthType=OAuth2. Secrets via SecretBundle key.
Webhook WebhookConfig (VO) Outbound webhooks (to tenant or vendor) including signing.
Permissions IReadOnlyList<PermissionGrant> Minimal permissions this connector needs (AccessScope/Type mapping).
DataEgressDomains IReadOnlyList<string> Allowed external domains; enforced by residency/provider policy.
Endpoints IReadOnlyList<EndpointRef> Connector APIs/base URLs by region.
CreatedAtUtc / UpdatedAtUtc DateTime Timestamps.

IOAuthClient (entity) { ClientId:string, SecretBundleKey:string, AuthUrl:Uri, TokenUrl:Uri, Scopes:string[], RedirectUris:Uri[], ExtraParams?:IDictionary<string,string> } (SecretBundleKey points to Secrets context; no plaintext stored.)

WebhookConfig (VO) { CallbackUrl:Uri, SignatureAlgorithm:WebhookSignatureAlgorithm (HmacSha256|HmacSha512), SecretBundleKey:string, ReplayWindowSeconds:int (e.g., 300), RetryPolicy:{ Max:int, Backoff:string } }

EndpointRef (VO) { RegionCode:string, BaseUrl:Uri }

PermissionGrant (VO) { AccessScope:string, AccessType:string, Effect:GrantEffect (Allow|Deny), ConditionExpression?:string } (AccessScope/Type map to your enumerations.)


IInstallation — properties (aggregate)

Property Type Description & rules
InstallationId Guid Aggregate id.
TenantId Guid Owning tenant.
ConnectorId Guid Connector being installed.
Status InstallationStatus (Pending,Active,Suspended,Revoked,Uninstalled) Lifecycle.
RegionCode string Must comply with tenant residency and connector endpoints.
AuthorizedAtUtc? DateTime? When OAuth/API key validated.
RevokedAtUtc? DateTime? When deauthorized.
Config IDictionary<string,string> Non-secret config (e.g., channel id).
SecretRefs IReadOnlyList<string> Secret bundle keys (e.g., per-tenant API keys).
ScopesGranted IReadOnlyList<PermissionGrant> Effective grants at installation time.
WebhookDeliveryState { LastDeliveryAtUtc?:DateTime, Failures:int } Telemetry for reliability.
CreatedAtUtc / UpdatedAtUtc DateTime Timestamps.

Enums

Enum Members
AppStatus Draft,Published,Deprecated,Retired
ConnectorStatus Draft,Published,Deprecated
InstallationStatus Pending,Active,Suspended,Revoked,Uninstalled
ConnectorAuthType OAuth2,APIKey,None
WebhookSignatureAlgorithm HmacSha256,HmacSha512
GrantEffect Allow,Deny

Invariants & policies

  • Scope mapping PermissionGrant.AccessScope/Type must map to the enumeration set; deny publish if any unknown/over-broad scopes are requested.
  • Residency & provider allowlist Connector DataEgressDomains must pass the region’s Residency/Provider policy; installations must use endpoints allowed for the tenant’s RegionCode.
  • Secret hygiene OAuth client secrets and webhook signing keys are held by SecretBundle only; events/logs carry no secrets. Rotation via Secrets context.
  • Unique installation Enforce uniqueness (TenantId, ConnectorId) for Active/Pending; allow historical Uninstalled records for audit.
  • Webhook integrity Validate signatures using HMAC over a canonical string {timestamp}.{eventId}.{body}; enforce ReplayWindowSeconds to reject replays.
  • Deauthorization Revoked installations must stop sending/receiving data immediately; purge tokens/secrets and emit connector.revoked.
  • Least privilege Granted permissions are the intersection of Connector permissions and App-required scopes and may be further reduced by tenant policy.

Domain events

  • app.published{ appKey, displayName, categories, requiredScopes[] }
  • app.deprecated{ appKey, sunsetAfterUtc }
  • connector.published|deprecated{ connectorId, appKey, key, authType }
  • installation.created{ installationId, tenantId, connectorId, regionCode, status }
  • connector.authorized{ installationId, authorizedAtUtc, authType }
  • connector.revoked{ installationId, revokedAtUtc, reason }
  • installation.deleted{ installationId, tenantId }
  • webhook.delivered|failed{ installationId, deliveryId, code, durationMs }

(Events include ids/keys and timestamps; no secrets or PII besides tenantId.)


Flows

Install (OAuth2)

  1. Admin selects Connector → CreateInstallation(tenantId, connectorId)Status=Pending.
  2. Redirect to provider auth (OAuthClient), receive code on redirect URI.
  3. Exchange code using SecretBundle client secret → store token SecretBundleKey in Installation.SecretRefs; set AuthorizedAtUtc, Status=Active.
  4. Emit connector.authorized and installation.created.

Install (API Key)

  • Store API key in SecretBundle; keep only the reference in Installation.

Uninstall / Revoke

  • Revoke tokens/keys, rotate webhook secret if shared, set Status=Uninstalled or Revoked; emit connector.revoked.

Webhook delivery

  • Sign with WebhookConfig secret; include headers: X-Signature, X-Timestamp, X-Event-Id. Retries with exponential backoff; stop after Max attempts and emit webhook.failed.

APIs / projections

Commands

  • RegisterApp(appKey, displayName, description, vendor, links…, requiredScopes[])
  • PublishApp(appKey) / DeprecateApp(appKey, sunsetAfterUtc)
  • RegisterConnector(appKey, key, authType, permissions[], endpoints[], webhook?, oauthClient?)
  • PublishConnector(connectorId) / DeprecateConnector(connectorId)
  • CreateInstallation(tenantId, connectorId, regionCode, config?)
  • AuthorizeConnector(installationId, authPayload) (OAuth code/API key ref)
  • RevokeConnector(installationId, reason)
  • UpdateInstallationConfig(installationId, config)
  • RotateWebhookSecret(connectorId) (roll signing key with overlap window)
  • DeleteInstallation(installationId) (soft-delete/archive)

Reads / Projections

  • MarketplaceCatalog (cards)

{ appKey, displayName, vendor, categories[], logoAssetRef?,
  connectors:[{ connectorId, key, displayName, authType, status, categories? }],
  rating?: { average, count }, tags[] }
* ConnectorDetails(connectorId) — permissions, endpoints, webhook info (redacted), install pre-checks (residency/providers). * InstallationView(installationId) — status, config (non-secret), scopes granted, last webhook delivery stats. * TenantInstallations(tenantId) — list + health.


ORM notes

  • Aggregates
    • IntegrationApp root; Connector root referencing AppKey.
    • Installation root linking to TenantId & ConnectorId; do not FK to Tenant table across contexts (store scalar ID).
    • OAuthClient as child entity under Connector; WebhookConfig as owned VO.
  • Uniqueness & indexes
    • Unique (Key) per IntegrationApp.
    • Unique (AppKey, Key) per Connector.
    • Unique filtered (TenantId, ConnectorId) for active/pending installations.
    • Index Installation(Status, UpdatedAtUtc DESC) for admin views.
  • Secrets
    • Only store SecretBundleKey strings for OAuth/webhook; never store plaintext tokens.

Integration touchpoints

  • Secrets: client secrets, access tokens, webhook signing secrets rotate centrally; connectors listen for rotation and refresh.
  • Data Residency & Routing: validate RegionCode and DataEgressDomains before activating an installation; block if policy denies provider endpoints.
  • Notifications: surface install success/failure to tenant Owner/Tech contacts; send webhook delivery failures to on-call.
  • Audit: every install/authorize/revoke event produces an audit record with traceId; webhook deliveries produce append-only audit entries (sans payload, with hashes).

Security & compliance

  • Enforce least privilege: permissions presented to the tenant admin explicitly; default-deny.
  • Require signed webhooks with short replay windows and idempotency keys; verify timestamps drift.
  • Support right-to-revocation: tenant admin can revoke installations at any time; system purges tokens/refresh tokens immediately.
  • Implement scoped rate limits per installation for webhook/API interactions to protect the platform and vendors.

This context enables a safe, governable marketplace with explicit permissions, strong residency controls, secret hygiene, and operationally solid webhook/auth flows—cleanly integrated with your existing Access, Secrets, Tenant, and Routing contexts.


Workflow Orchestration (Deterministic) Context

Relationships

  • WorkflowDefinition HasMany Triggers & Tasks A definition is a versioned, deterministic DAG of Tasks, plus one or more Triggers (CRON/HTTP/Event/Manual).
  • RunInstance HasMany TaskExecutions Each run materializes the DAG with task-state, attempts, outputs, and approvals.
  • Tasks Reference CompensationSpec / ApprovalGate Side-effecting tasks declare compensations; gated tasks declare approvals.

Types in this context

  • Aggregates: IWorkflowDefinition, IRunInstance, ITrigger
  • Entities/VOs: IDefinitionTask (entity), ITaskExecution (entity), ApprovalGate (VO), CompensationSpec (VO), RetryPolicy (VO), TimeoutPolicy (VO), OwnerRef (VO)
  • Enums: RunState, TaskStatus, TaskKind, TriggerType, ApprovalOutcome

Cross-context calls are executed by activities that must be idempotent and authorized via Access Rules. No AI tools here—this is the deterministic engine.


IWorkflowDefinition — properties (contract + semantics)

Property Type Description & rules
WorkflowDefinitionId Guid Aggregate id.
Key string Slug (e.g., provision.tenant.v3). Unique per environment.
Version int Monotonic version. Publishing creates a new immutable version.
DisplayName string Human label.
Description string Purpose and business guarantees.
Owner OwnerRef (VO) Team/rotation responsible for the workflow.
Tasks IList<IDefinitionTask> Deterministic DAG (topologically valid).
Triggers IList<ITrigger> CRON/HTTP/Event triggers (see below).
DefaultRetry RetryPolicy (VO) Fallback for tasks without explicit retry.
DefaultTimeout TimeoutPolicy (VO) Global activity timeout bounds.
Parameters IReadOnlyList<{Name:string,Type:string,Required:bool,Default?:string}> Run-time inputs.
State string (Draft,Published,Deprecated) Lifecycle; only Published can start new runs.
CreatedAtUtc/UpdatedAtUtc DateTime Timestamps.

IDefinitionTask (entity)

Field Type Notes
TaskKey string Unique within definition.
Kind TaskKind (Activity,Timer,Gate,SubWorkflow) Execution semantics.
Action string Activity slug (e.g., http.post, kv.put, billing.prorate). Empty for Gate/Timer.
Parameters IDictionary<string,string> Templated with inputs/outputs ({{run.input.tenantId}}).
DependsOn IReadOnlyList<string> Upstream TaskKeys. Must not create cycles.
Retry? RetryPolicy? Overrides default retry.
Timeout? TimeoutPolicy? Overrides default timeout.
Approval? ApprovalGate? Required when Kind=Gate.
Compensation? CompensationSpec? How to undo this task’s side effects.
IdempotencyKeyExpr? string? Deterministic expression for activity idempotency (e.g., {{run.id}}:{{TaskKey}}).

ITrigger — properties

Property Type Description
TriggerId Guid Identifier.
Type TriggerType (Cron,Http,Event,Manual) Trigger kind.
Cron? string? CRON expression (Cron only).
Path? string? HTTP path (Http only).
EventKey? string? Event routing key (Event only).
AuthScope string Access scope required to invoke (Http/Manual).
Enabled bool Toggle.

IRunInstance — properties

Property Type Description & rules
RunInstanceId Guid Aggregate id.
DefinitionKey / Version string / int The published definition snapshot.
CorrelationId string For dedupe and cross-system tracing.
State RunState (Pending,Running,Completed,Failed,Canceled,Compensating,Compensated) Run lifecycle.
Input IDictionary<string,string> Immutable inputs, validated against schema.
Outputs IDictionary<string,string> Final run outputs (composed from tasks).
StartedAtUtc / CompletedAtUtc? DateTime Timeline.
TaskExecutions IList<ITaskExecution> Materialized DAG state.
Owner OwnerRef (VO) Current operator/queue (for approvals, manual actions).

ITaskExecution (entity)

Field Type Notes
TaskKey string Link to definition.
Status TaskStatus (Pending,Running,Succeeded,Failed,WaitingApproval,TimedOut,Skipped,Compensated) State machine.
Attempt int Attempt counter.
StartedAtUtc? / EndedAtUtc? DateTime? Timing.
Output IDictionary<string,string> Small structured outputs (ids, refs).
Error? { Code:string, Message:string, Retryable:bool }? Last error.
ApprovalOutcome? ApprovalOutcome? Set for gates.
ChildRunId? Guid? For SubWorkflow tasks.
IdempotencyKey string Effective key used by the executor.

Value objects

ApprovalGate (VO){ Approvers:[OwnerRef], Rule:"any|all|quorum:n", Timeout:TimeSpan, EscalateTo?:OwnerRef } CompensationSpec (VO){ Action:string, Parameters:IDictionary<string,string>, When:"on-failure"|"always" } RetryPolicy (VO){ MaxAttempts:int, Backoff:"exponential|fixed", InitialDelay:TimeSpan, Jitter:boolean } TimeoutPolicy (VO){ Soft:TimeSpan, Hard:TimeSpan } OwnerRef (VO){ Team:string, OnCallRotation?:string, AgentId?:string }


Enums

Enum Members
RunState Pending,Running,Completed,Failed,Canceled,Compensating,Compensated
TaskStatus Pending,Running,Succeeded,Failed,WaitingApproval,TimedOut,Skipped,Compensated
TaskKind Activity,Timer,Gate,SubWorkflow
TriggerType Cron,Http,Event,Manual
ApprovalOutcome Approved,Rejected,Expired

Invariants & policies

  • Deterministic DAG: Tasks must be cycle-free; Parameters are pure functions of inputs/previous outputs. No network time or randomness in definitions.
  • Exactly-once activities: Every Activity resolves an idempotency key; executors must use provider idempotency (e.g., Idempotency-Key header) and handle at-least-once delivery.
  • Authorization: Triggers and activities evaluate Access Rules (scope/type). Http triggers require signed requests; Event triggers verify origin.
  • Time discipline: Soft timeouts mark tasks for cancellation; Hard enforces termination. Timeouts generate compensations when configured.
  • Approvals: Gate tasks block until Approved/Rejected/Expired; SLA on approvals can raise incidents or escalate per ApprovalGate.
  • Compensation: If the run fails and a task has CompensationSpec, the engine executes compensations in reverse topological order for succeeded side-effects.

Domain events

  • workflow.published{ key, version, taskCount, triggerCount }
  • workflow.deprecated{ key, version }
  • run.started{ runId, key, version, correlationId }
  • task.started|succeeded|failed|timed_out{ runId, taskKey, attempt, idempotencyKey, timings }
  • gate.waiting{ runId, taskKey, approvers[], deadlineUtc }
  • gate.approved|rejected|expired{ runId, taskKey, outcome, who?, whenUtc }
  • run.completed|failed|canceled{ runId, key, version, outputs? }
  • run.compensating|compensated{ runId, compensatedTasks }

Events include IDs, keys, and small metadata only—no secret payloads. Correlate with correlationId and traceId.


APIs / projections

Commands

  • PublishWorkflow(defKey, version, tasks[], triggers[], defaults)
  • DeprecateWorkflow(defKey, version)
  • StartRun(defKey@version, input, correlationId?)
  • CancelRun(runId, reason)
  • Approve(runId, taskKey, approver) / Reject(runId, taskKey, approver)
  • Signal(runId, taskKey, payload) (for event-driven tasks)
  • RetryTask(runId, taskKey) (if retryable and within limits)

Reads

  • RunTimelineView(runId)

{ runId, key, version, state, startedAtUtc, completedAtUtc?,
  tasks:[{ taskKey,status,attempt,startedAtUtc?,endedAtUtc?,error?,approvalOutcome?,childRunId? }],
  outputs, correlationId }
* WorkflowCatalog — definitions, versions, triggers, owners. * PendingApprovalsView(owner/team) — gates awaiting action with deadlines. * ExecutionMetrics — success rates, durations, retries per task.


Executor design (handler guidance)

  • Planner loads Definition + validates inputs → materializes TaskExecutions in Pending.
  • Scheduler picks runnable tasks (all DependsOn succeeded), acquires a short lease, sets Running, computes IdempotencyKey, executes with retry policy, writes outputs, and transitions on success/failure/timeout.
  • Gatekeeper manages approvals (emails/Slack/portal), updates status on decision/expiry.
  • Compensator builds reverse plan when failing and executes CompensationSpec activities with their own idempotency keys.
  • Idempotency: executor must be restart-safe; replays use the same IdempotencyKey and become no-ops at providers.

ORM notes

  • Aggregates
    • WorkflowDefinition root; DefinitionTask and Trigger as child tables (FK DefinitionId), immutable once Published.
    • RunInstance root; TaskExecution child table with (RunId, TaskKey) unique.
  • Indexes
    • RunInstance(State, StartedAtUtc DESC) for dashboards.
    • TaskExecution(RunId, Status) for picking next work.
    • Trigger(Type, Enabled) for schedulers; Http(Path) unique.
  • Constraints
    • Check DAG acyclicity on publish (server-side validation).
    • Enforce Version immutability after publish.

Integration touchpoints

  • Access Rules: triggers and activities check AccessScope/Type before executing.
  • Secrets: activities receive secret references only; engine resolves via Secrets context with short-lived tokens.
  • Provisioning / Billing / Notifications: common activity adapters (provisioning.start, billing.create_invoice, notify.send) executed idempotently.
  • Observability: emit metrics for task.duration, task.retries, run.state; integrate with Incident Mgmt for chronic failures/timeouts.

Security & compliance

  • Signed triggers: HTTP requires HMAC signatures + timestamp; Event triggers validate origin & schema.
  • Audit-everything: publish audit records for run/approval/compensation transitions with traceId.
  • Least privilege: service principals executing activities have scoped, time-bound grants.
  • PII-safety: inputs/outputs are metadata; large or sensitive payloads move through content stores by reference.

This context gives you a deterministic, idempotent workflow engine with first-class approvals and compensations, clean DDD boundaries, and operational guardrails that make automations safe and auditable across the platform.


Feature Flagging & Experimentation (Runtime) Context

Relationships

  • FlagDefinition HasMany TargetingRules Rules implement audience targeting, percentage rollouts, and variant allocation.
  • Experiment References FlagDefinition Experiments attach to a flag (boolean or multivariate) and control who gets which variant for a period.
  • Experiment HasMany Exposures (append-only) Each exposure records the resolved assignment at evaluation time for analytics.

Types in this context

  • Aggregates: IFlagDefinition, IExperiment, ITargetingRule
  • Entities/VOs: IExposure (entity), EvaluationContext (VO), BucketingKey (VO), Variant (VO), OwnerRef (VO), Guardrail (VO)
  • Enums: FlagStatus, FlagType, ExperimentStatus, RunMode, UnitType

This context is runtime-first: sub-millisecond evaluation, edge-cache friendly, and entitlement/residency aware. Cross-context references are keys/ids only.


IFlagDefinition — properties (contract + semantics)

Property Type Description & rules
FlagId Guid Aggregate id.
Key string Canonical slug ("billing.api.rate_limit.new_eval"). Immutable once Active.
DisplayName string Human label.
Description string What this flag controls and safety notes.
Type FlagType (Boolean,Multivariate,JSONConfig) Evaluation output domain.
DefaultVariant Variant (VO) Fallback when no rules/experiments match. Must be one of Variants.
Variants IReadOnlyList<Variant> Allowed outputs (e.g., on/off, A/B, or JSON blobs for config).
Rules IList<ITargetingRule> Ordered top-to-bottom. First match wins unless ContinueIf specified.
KillSwitch bool If true, force DefaultVariant for everyone (used during incidents).
Status FlagStatus (Draft,Active,Deprecated,Killed) Lifecycle.
Owner OwnerRef (VO) Team/rotation accountable for the flag.
Tags IReadOnlyList<KeyValueTag> Domain, product, residency risk level.
CreatedAtUtc / UpdatedAtUtc DateTime Timestamps.

Variant (VO){ Key:string, Value?:string } (For JSONConfig, Value is a small JSON string validated by a stored schema.)


ITargetingRule — properties (aggregate)

Property Type Description & rules
RuleId Guid Aggregate id.
Priority int Evaluation order (lower = earlier).
Name string Human label ("EU tenants 10% rollout").
Condition string CEL/OPA-like expression over EvaluationContext (see below).
UnitType UnitType (User,Tenant,Session) Bucketing unit for stickiness.
Allocation IReadOnlyList<{ VariantKey:string, Percent:int }> Must sum to 100 when used with bucketing.
BucketSalt string Salt for deterministic hashing. Changing it re-buckets the population.
ContinueIf bool When true, continue evaluating next rules (multi-hit use cases); default false.
EffectiveDate / ExpiryDate? DateTime / DateTime? Temporal window (UTC).
ResidencyConstraint? { RegionCodes:string[] }? Optional additional residency guard.
EntitlementConstraint? { EditionKeys?:string[], FeatureKeys?:string[] }? Ensure rule never grants beyond entitlements.
Status string (Draft,Active,Paused) Operational toggle.
UpdatedAtUtc DateTime For cache invalidation.

IExperiment — properties (aggregate)

Property Type Description & rules
ExperimentId Guid Aggregate id.
Key string Slug ("checkout-new-flow-ab-v4").
FlagKey string Target flag (by key). Flag Type and experiment Variants must be compatible.
DisplayName string Human label.
Hypothesis string Short statement of expected outcome.
UnitType UnitType Bucketing unit—must match the main targeting rule for consistency.
Namespace string Optional global namespace to avoid overlap with other experiments.
Allocation IReadOnlyList<{ VariantKey:string, Percent:int }> A/B(/n) split. Sums to 100.
SampleFraction double (0–1] Fraction of eligible population to include (e.g., 0.5 for 50%).
BucketingKey BucketingKey (VO) Deterministic key derivation expression.
Metrics IReadOnlyList<{ Key:string, Kind:"Primary" | "Secondary", Direction:"Increase" | "Decrease" }> For reporting only (no stats engine here).
Guardrails IReadOnlyList<Guardrail> Stop conditions (e.g., error rate > x%).
ResidencyConstraint? { RegionCodes:string[] }? Additional constraint; default inherits from flag/rule.
EntitlementConstraint? { EditionKeys?:string[], FeatureKeys?:string[] }? Tenant must be entitled or excluded.
RunMode RunMode (Shadow,Live) Shadow evaluates but does not expose variant to client.
Status ExperimentStatus (Draft,Running,Paused,Stopped,Completed) Lifecycle.
StartAtUtc / EndAtUtc? DateTime / DateTime? Schedule.
CreatedAtUtc / UpdatedAtUtc DateTime Timestamps.

BucketingKey (VO){ Expr:string, Salt:string, Algo:"murmur3"|"xxhash" } (Example Expr: "tenantId" for tenant stickiness or "tenantId:userId" for user stickiness under tenant.)

Guardrail (VO){ MetricKey:string, Threshold:double, Window:TimeSpan, Action:"Pause"|"Stop" }


EvaluationContext (VO)

Field Type Description
TenantId Guid? Optional for anonymous flows; required for tenant-scoped flags.
UserId Guid? Optional; use when UnitType=User.
SessionId string? For session bucketing.
RegionCode string? Used for residency constraints.
EditionKey? string? Current tenant edition snapshot.
EntitledFeatureKeys? IReadOnlyList<string>? Effective entitlements (from Config/Identity projection).
Attributes IDictionary<string,string> Arbitrary attributes (e.g., appVersion, device, locale).
RequestTimeUtc DateTime Server-evaluated current time (no client manipulation).

Security note: No raw PII is required for evaluation; stickiness uses opaque ids.


IExposure — properties (entity, append-only)

Property Type Description & rules
ExposureId string (ULID) Row id; ensures global ordering per time.
ExperimentId Guid What experiment this exposure belongs to.
FlagKey string For convenience in analytics.
UnitType UnitType Bucketing unit used.
UnitId string Hashed unit identifier (tenantId, userId, or sessionId).
TenantId? Guid? For tenant-scoped analytics.
VariantKey string Assigned variant.
ContextHash string Hash of attributes used in decision (tamper defense).
EvaluatedAtUtc DateTime Timestamp.
RequestId? string? Optional correlation id.

Enums

Enum Members
FlagStatus Draft,Active,Deprecated,Killed
FlagType Boolean,Multivariate,JSONConfig
ExperimentStatus Draft,Running,Paused,Stopped,Completed
RunMode Shadow,Live
UnitType User,Tenant,Session

Invariants & policies

  • Precedence (highest → lowest): KillSwitchTenant overrideEdition defaultProduct defaultExperiment allocationDefaultVariant. (Tenant overrides live in Config context; the evaluator consumes the projection.)
  • Entitlement guard A rule/experiment must not assign a variant that enables a Feature the tenant/user is not entitled to. If violated, evaluator must fallback to an entitled variant or DefaultVariant.
  • Residency guard Evaluations must respect regional constraints: if RegionCode not in allowed set, fallback (no exposure recorded).
  • Determinism & stickiness Same (UnitId, FlagKey, Salt) → same variant across evaluations until allocation changes.
  • Allocation validity Percentages in an Allocation sum to 100; no negative or zero for all variants.
  • Temporal windows Rules and experiments apply only within [EffectiveDate, ExpiryDate) / [StartAtUtc, EndAtUtc).
  • Killed flags Status=Killed forces DefaultVariant and bypasses experiments—used during incidents.
  • Privacy Do not store raw UserId/SessionId in exposures; store hashed UnitId. PII minimization by default.

Domain events

  • flag.created{ flagId, key, type, variants[], defaultVariant }
  • flag.changed{ flagId, key, changes:{ rules?, variants?, status? } }
  • flag.killed{ flagId, key, reason }
  • experiment.started{ experimentId, key, flagKey, unitType, allocation, sampleFraction, startAtUtc }
  • experiment.paused|stopped|completed{ experimentId, key, atUtc, reason? }
  • exposure.recorded{ experimentId, flagKey, unitType, unitIdHash, variantKey, evaluatedAtUtc }

Events include metadata only; no raw context attributes beyond hashed unit keys.


APIs / projections

Low-latency Evaluate

  • Evaluate(flagKey, evaluationContext)

{ variant:{ key, value? }, source:"Override|Rule|Experiment|Default",
  debug?:{ matchedRuleId?, bucket?:{ unitType, unitIdHash, salt, pct } } }
* EvaluateMany(flagKeys[], evaluationContext) → batch for edge efficiency.

Management

  • CreateFlag(key, type, variants[], defaultVariant, owner, tags?)
  • PublishFlag(flagId) / KillFlag(flagId, reason)
  • UpsertRule(flagKey, rule) / PauseRule(flagKey, ruleId) / DeleteRule(flagKey, ruleId)
  • StartExperiment(flagKey, experiment) / PauseExperiment(experimentId) / StopExperiment(experimentId) / CompleteExperiment(experimentId)

Reads / Projections

  • FlagSnapshot(region) — compact, signed snapshot for edge nodes/CDN.
  • ExperimentView(experimentId) — status, allocations, exposures, guardrail status.
  • ExposureSummary(experimentId, window) — counts by variant, by region/edition/tenant segment.
  • DecisionLog(flagKey, window) — sampled debug decisions (off by default).

Evaluator design (operational notes)

  • Critical path: pure, allocation-compatible function:
    1. If KillSwitch, return default.
    2. Apply tenant overrides (from Config projection).
    3. Apply rules in priority order (predicate on EvaluationContext).
    4. If experiment Running and unit in sample, perform allocation via hash(bucketingKey + salt) % 100 and pick variant.
    5. Enforce entitlement/residency guards before returning.
  • Caching:
    • Edge nodes keep FlagSnapshot (ETag’ed, signed) with short TTL (e.g., 30s) and event-driven invalidation on flag.changed.
    • Decision results for hot keys can be memoized per (flagKey, unitIdHash) for a few seconds.
  • Exposures:
    • Recorded asynchronously (fire-and-forget) to a write-optimized log (e.g., Kafka) → ingested to the Data Platform.
    • Deduplicate on (experimentId, unitIdHash) per window to avoid double counting.

ORM notes

  • Aggregates
    • FlagDefinition root with child TargetingRule rows (ordered by Priority).
    • Experiment root referencing FlagKey.
    • Exposure as append-only time-series table/stream (partition by experimentId or tenantId).
  • Indexes
    • FlagDefinition(Key) unique; TargetingRule(FlagId, Status, Priority);
    • Experiment(FlagKey, Status);
    • Exposure(ExperimentId, EvaluatedAtUtc) and optionally (ExperimentId, UnitId) for dedupe.
  • Migrations
    • Flag Key immutable after Active; variant set can only grow (removal requires deprecation cycle).

Integration touchpoints

  • Config & Entitlements: evaluator consumes tenant overrides and entitlement projection; never mutates them.
  • Identity: EvaluationContext may include roles/claims for ABAC-style rules (as attributes).
  • Data Residency: region code from Routing; evaluator blocks cross-region activations where disallowed.
  • Notifications: optional announcement when experiments start/stop (internal visibility).
  • Data Platform: exposures stream into datasets for analysis; reports compute lift/guardrail breaches.

Security & compliance

  • No PII in evaluation/exposures; use hashed/stable ids.
  • Signed snapshots to prevent edge tampering; clients cannot set server-sourced attributes (e.g., RegionCode).
  • Audit changes to flags/rules/experiments with traceId, author, and diffs.
  • Kill switch available to Incident Mgmt to instantly revert unsafe flags globally.

This context gives you a fast, safe, and compliant runtime for feature gating and experimentation—coexisting with Config flags, honoring entitlements and residency, and producing clean exposure data for trustworthy analysis.


Content & Branding Context

Relationships

  • BrandPack HasMany Assets A brand pack is a versioned set of visual assets and theming VOs.
  • ContentPack HasMany Localized entries A content pack holds localized strings and templates (email/UI copy).
  • Tenant → BrandPack / ContentPack Packs can be tenant-scoped (override) or global defaults; lookup resolves with fallback (Tenant → Global) and locale fallback.

Types in this context

  • Aggregates: IBrandPack, IContentPack
  • Entities/VOs: IAsset (entity), ILocalizedContent (entity), Locale (VO), Checksum (VO), ColorPalette (VO), TypographySpec (VO), TemplateRef (VO)
  • Enums: PackStatus, AssetKind, TemplateKind, ContentFormat

Binary content lives in object storage; the domain stores metadata + pointers. Signed URLs are issued at read time.


IBrandPack — properties

Property Type Description & rules
BrandPackId Guid Aggregate id.
Key string Slug (e.g., default.light.v1, tenant-acme.v2). Immutable after publish.
TenantId? Guid? null for global; value for tenant-scoped override.
DisplayName string Human label for admins/designers.
Version int Monotonic; publishing increments; immutable once Published.
Status PackStatus (Draft,Published,Deprecated) Lifecycle.
DefaultLocale Locale (VO) Fallback locale (e.g., en-US). Must be in SupportedLocales.
SupportedLocales IReadOnlyList<Locale> Locales this pack provides.
Theme ColorPalette + TypographySpec Visual theme (owned VOs).
Assets IList<IAsset> Logos/icons/illustrations/email partials, etc.
CreatedAtUtc / UpdatedAtUtc DateTime Timestamps.
Tags IReadOnlyList<KeyValueTag> Metadata (brand, dark-mode, product area).

ColorPalette (VO){ Primary:string, OnPrimary:string, Secondary:string, OnSecondary:string, Background:string, Surface:string, Success:string, Warning:string, Danger:string } (hex #RRGGBB validated; contrast responsibility on UI layer). TypographySpec (VO){ FontFamily:string, FontUrls?:string[], BaseSizePx:int, Scale:{ H1:int, H2:int, H3:int, H4:int, H5:int, H6:int } }


IAsset — properties (child entity)

Property Type Description & rules
AssetId Guid Identity for updates/rotation.
Kind AssetKind (LogoLight,LogoDark,Icon,Illustration,EmailPartial,Stylesheet) Controls placement/usage.
Name string Human label.
Locale? Locale? Optional locale for localized imagery (e.g., RTL logos).
MimeType string Whitelist (e.g., image/svg+xml, image/png, text/css).
SizeBytes long Hard-capped (e.g., ≤ 2 MB images, ≤ 200 KB CSS).
Width? / Height? int? For raster assets; aids responsive layout.
StorageUrl string Object storage URI (not directly public).
Checksum Checksum (VO) Content hash for cache busting/integrity.
CreatedAtUtc DateTime Upload time.
Active bool Deactivate without deleting (for rollbacks).

Checksum (VO){ Algo:"sha256", Value:string } Locale (VO){ Code:string } (BCP-47; validated)


IContentPack — properties

Property Type Description & rules
ContentPackId Guid Aggregate id.
Key string Slug (e.g., emails.core.v3, portal.copy.v2). Immutable after publish.
TenantId? Guid? null for global; value for tenant-scoped overrides.
DisplayName string Human label.
Version int Monotonic; immutable once Published.
Status PackStatus (Draft,Published,Deprecated) Lifecycle.
DefaultLocale Locale Fallback locale.
SupportedLocales IReadOnlyList<Locale> Locales present.
Entries IList<ILocalizedContent> Localized content items (strings/templates).
CreatedAtUtc / UpdatedAtUtc DateTime Timestamps.
Tags IReadOnlyList<KeyValueTag> Metadata (channel, product).

ILocalizedContent (entity)

Field Type Notes
ContentId Guid Identity.
ContentKey string Stable key (e.g., email.welcome.subject). Unique per (Key, Locale).
Locale Locale Language/region.
Format ContentFormat (PlainText,Markdown,Html,Mjml,Json) Rendering pipeline.
BodyRef TemplateRef (VO) Pointer to body (object storage) or inline string for small content.
VariablesSchema? string? (JSON Schema) Required variables and types for rendering (for safety).
Checksum Checksum Integrity/caching.
CreatedAtUtc DateTime Timestamp.
Active bool If false, entry ignored at resolve time.

TemplateRef (VO){ StorageUrl?:string, Inline?:string } (only one populated)


Enums

Enum Members
PackStatus Draft,Published,Deprecated
AssetKind LogoLight,LogoDark,Icon,Illustration,EmailPartial,Stylesheet
TemplateKind Email,Notification,UiCopy
ContentFormat PlainText,Markdown,Html,Mjml,Json

Invariants & policies

  • Tenant isolation A tenant-scoped pack (TenantId!=null) may only be resolved/served for that tenant; RLS enforced on reads.
  • Version immutability Once Status=Published, entries/assets in that version are immutable; changes require a new version.
  • Locale discipline DefaultLocale ∈ SupportedLocales; missing locale resolution uses fallback: TenantPack(locale)TenantPack(default)GlobalPack(locale)GlobalPack(default).
  • Size & type limits Enforce MIME allowlist and maximum sizes; raster dimensions must be within configured bounds.
  • Sanitization Html/Mjml bodies are sanitized at render time; disallow inline scripts and remote JS. Images served via signed URLs.
  • Residency Assets/content stored in a region aligned with Tenant.RegionResidency; signed URLs are region-bound.
  • Variables safety Rendering requires values conforming to VariablesSchema; unknown variables ignored, missing required → render fails fast.

Domain events

  • brandpack.published{ brandPackId, key, version, tenantId?, locales[], theme:{…} }
  • brandpack.deprecated{ brandPackId, key, version, sunsetAfterUtc }
  • contentpack.published{ contentPackId, key, version, tenantId?, locales[], entryCount }
  • contentpack.updated{ contentPackId, key, version, changes:{ added:int, updated:int, deactivated:int } }

(Events contain metadata only—no binaries or template bodies.)


APIs / projections

Commands

  • CreateBrandPack(key, tenantId?, defaultLocale, supportedLocales[], theme)
  • AddAsset(brandPackId, kind, name, locale?, mimeType, sizeBytes, storageUrl, checksum, width?, height?)
  • PublishBrandPack(brandPackId) / DeprecateBrandPack(brandPackId, sunsetAfterUtc)
  • CreateContentPack(key, tenantId?, defaultLocale, supportedLocales[])
  • PutContentEntry(contentPackId, contentKey, locale, format, bodyRef, variablesSchema?)
  • PublishContentPack(contentPackId) / DeprecateContentPack(contentPackId)
  • RotateAsset(brandPackId, assetId, storageUrl, checksum) (creates new pack version)

Reads / Projections

  • BrandingView(tenantId, locale)

{ theme:{ colors:{...}, typography:{...} },
  logos:{ light:signedUrl, dark:signedUrl, icon:signedUrl },
  locale:"en-US", source:{ tenantPackKey?, globalPackKey } }
* ContentResolve(contentPackKey, contentKey, locale, variables){ format, bodyRendered, subject? } * SignedAssetUrl(assetId, ttl) → short-lived URL * PackCatalog(tenantId?) — visible packs (tenant + global), versions, statuses.


ORM notes

  • Aggregates
    • BrandPack root with owned VOs (Theme, Locales); Asset child table (FK BrandPackId).
    • ContentPack root with LocalizedContent child table (FK ContentPackId).
  • Constraints & indexes
    • Unique (Key, Version, TenantId?) for packs.
    • Unique (ContentPackId, ContentKey, Locale.Code) for entries.
    • Index Asset(BrandPackId, Kind, Locale.Code) for fast logo/icon lookup.
    • Store TemplateRef.Inline only for small payloads; otherwise StorageUrl to object store.
  • No cross-context FKs
    • Only scalar TenantId stored; other contexts consume projections.

Integration touchpoints

  • Notifications: requests ContentResolve for email subject/body (MJML → HTML), and BrandingView for correct logos/colors in templates.
  • Web Portal: initializes theme from BrandingView; caches signed URLs (short TTL).
  • Marketplace: app/connector cards pull logos/icons via signed URLs.
  • Secrets: signing keys for URL generation managed in Secrets & Provider Credentials.
  • Data Residency & Routing: content delivery endpoints are region-aware; CDN caches are partitioned by region and tenant.

Security & compliance

  • Signed URLs only (short-lived, single-use where needed); no public buckets for tenant assets.
  • Sanitize HTML templates; block script tags/inline event handlers.
  • Audit pack publishes/deplications with diffs and traceId.
  • Right-to-erasure: avoid PII in templates/assets; if present, provide redaction/rotation paths in object storage.

Operator guidance

  • Keep global default packs lean and neutral; push heavy assets (illustrations) behind feature flags in the portal.
  • For high-contrast/dark mode support, ensure both LogoLight and LogoDark exist; portals switch based on media queries.
  • Version frequently and never mutate published packs; rely on cache-busting via Checksum.

This context gives Notifications and Web a consistent, secure source of brand and content truth, with tenant isolation, regional compliance, robust versioning, and fast resolution APIs.