ConnectSoft SaaS Platform — DDD Design Blueprint¶
Introduction & Principles¶
Why DDD for the ConnectSoft SaaS Platform¶
Domain-Driven Design (DDD) lets us scale a multi-product, multi-tenant platform without turning into a “big ball of services.” We model the business—tenants, products, editions, entitlements, billing, compliance—as explicit domains with clear boundaries, contracts, and ownership. That makes the platform evolvable (new products/editions), compliant (auditability, data residency), and safe for automation/AI (guardrails and event contracts).
Strategic DDD (the “where” and “why”)¶
- Bounded Contexts (BCs): Each major capability (Tenant Management, Identity, SaaS Core Metadata, Billing, Config/Flags, Usage/Metering, Notifications, Audit/Compliance, AI Orchestration) is its own BC with its own model and language.
- Context Map: We define the relationship between BCs (upstream/downstream, conformist, anti-corruption layer, published language, customer/supplier) and prefer event-first integration.
- Ubiquitous Language (UL): Shared, versioned vocabulary per BC (e.g., Subscription, Edition, Entitlement, Quota). Terms never cross BCs implicitly; if they do, they cross through contracts.
- Team Topologies alignment: Each BC is owned by a long-lived team (clear “code + contracts + SLOs” ownership).
Tactical DDD (the “how”)¶
- Aggregates & Roots: The smallest consistency boundary protecting invariants. Examples:
Tenant,Edition,Subscription,FlagSet,UsageMeter,AuditStream. - Entities & Value Objects: Entities have identity; VOs are immutable and equality-by-value (e.g.,
Money,Quota,Residency,EvaluationContext). - Domain Events: Each aggregate emits domain events (e.g.,
tenant.created,subscription.canceled) captured in an Outbox for reliable delivery. - Repositories & Specifications: Repositories load/save aggregate roots only; query use-cases rely on read models or whitelisted Specification objects (sorting/filtering/paging).
- Domain Services: Encapsulate domain logic that doesn’t fit a single aggregate (e.g., proration rules). They are pure and state-free.
- Application Services: Orchestrate commands/transactions across aggregates, call domain services, emit integration events, enforce authorization and idempotency.
Event-First Integrations & Anti-Corruption Layers¶
- Event-First: State changes publish domain events to a durable bus. Downstream BCs build projections and trigger policies.
- Commands/Queries: Cross-context commands are rare and fronted by ACLs; cross-context queries read published read models or documented APIs.
- Anti-Corruption Layer (ACL): When integrating with external systems (payments, email/SMS, analytics) or another BC’s public model, the ACL translates foreign concepts to our UL and shields the domain from leakage.
Isolation, Consistency & Transactions¶
- Strong inside, eventual outside: Aggregates enforce strong invariants inside a BC; cross-BC effects achieve eventual consistency via outbox/inbox, retry, and idempotency keys.
- One command → one aggregate: Prefer commands that mutate exactly one aggregate root; multi-aggregate workflows use sagas/process managers at the application layer.
- Versioning: Events and contracts are up-versioned (additive first). Breaking changes require new topics/paths.
Modeling Conventions (cross-cutting)¶
Identifiers
TenantId,UserId,SubscriptionId, etc. use ULID/GUID (string) and are opaque to clients.- Compound keys are modeled as VOs (
TenantScopedId { tenantId, localId }) when needed. - Public IDs vs internal IDs: public are stable and expose no internals; internal may change.
Multi-Tenancy
- Every storage and message path considers
tenantIdand (when applicable)regionResidency. - Reads/writes are tenant-scoped at the repository boundary; cross-tenant operations are explicit admin flows.
Invariants
- Declared in aggregate constructors/mutators; validated before state mutation.
- Examples:
Subscriptioncannot activate without a validEdition;TenantResidencycannot change after lock;FlagKeyuniqueness per scope.
Enumerations
- Use smart-enum pattern for business concepts:
BillingCycle,PricingType,LimitResetPeriod,DeliveryStatus,LifecycleStatus,Scope. - Enumerations are part of the UL and versioned like contracts.
Value Objects
- Immutable; factory methods validate constraints (e.g.,
Money.Create(currency, amount),Quota.Create(dim, limit)). - Provide behavior, not just data (e.g.,
Money.Add,Window.Contains).
Domain vs Application Services
- Domain Service: implements business policy (pure functions, deterministic).
- Application Service: orchestrates use-cases (authorization, transactions, outbox, retries).
Repositories
- One repository per aggregate root type.
- No ad-hoc dynamic queries. For read scenarios, expose read models or specification-based queries with whitelisting.
Events
- Name:
<noun>.<pastParticiple>(e.g.,subscription.canceled). - Contract includes:
eventId,occurredAt (UTC),aggregateId,tenantId,version,payload. - Persist via Outbox; delivered with at-least-once semantics; consumers are idempotent.
Policies & Sagas
- Policies subscribe to events and issue commands (e.g.,
subscription.activated→ “grant entitlements”). - Sagas handle long-running, multi-step workflows with compensations (e.g., “provision tenant” → rollback on failure).
Security & Compliance
- Authorization at application service boundary (RBAC/ABAC), enriched with tenant, edition, entitlements.
- Data classification + redaction applied in logging/events/audit.
- Audit events correlate with request/trace IDs.
“How to read this document”¶
- Cycle-by-cycle: Each subsequent section focuses on one bounded context, starting with a Context Summary, then:
- Ubiquitous Language (terms),
- Aggregates / Entities / VOs / Enums,
- Commands & Events,
- Policies/Sagas,
- Repository & Read Model,
- Contracts (API/events),
- Invariants & State Machines,
- Security & Compliance notes,
- Evolution & Versioning.
- Traceability: Every contract and event references the owning BC and version. Cross-links to upstream/downstream BCs are called out explicitly.
- Copy-paste ready: Code skeletons and diagrams are provided where relevant so teams can implement directly.
Glossary (seed)¶
- Aggregate (Root): Cluster of domain objects with a single entry point that guards invariants.
- Anti-Corruption Layer (ACL): Translation layer that protects a domain model from external/foreign models.
- Bounded Context (BC): Explicit boundary where a particular domain model applies.
- Conformist: A downstream BC that adopts the upstream’s published language to reduce translation cost.
- Domain Event: A fact that something significant happened in the domain, emitted by aggregates.
- Domain Service: Stateless business logic that doesn’t belong to a single aggregate.
- Outbox/Inbox: Reliable messaging patterns for event publishing/consumption with idempotency.
- Policy: An event handler that reacts to domain events and issues commands.
- Saga/Process Manager: Orchestrates multi-step, long-running workflows across aggregates/BCs.
- Ubiquitous Language (UL): Shared vocabulary used by domain experts and developers within a BC.
Strategic Capabilities, Bounded Contexts & Context Map¶
Platform Capabilities and Bounded Contexts¶
The SaaS platform is structured as a set of well-defined bounded contexts, each owning its own ubiquitous language, models, and contracts. Together they form the backbone of the ConnectSoft factory approach, ensuring isolation, scalability, and safe evolution.
- Tenant Management — lifecycle of organizations using the platform. Responsible for tenant creation, region residency, lifecycle state, and contact data.
- Identity — authentication, authorization, roles, scopes, service principals, and federation. Issues tokens enriched with tenant, edition, and entitlements.
- SaaS Core Metadata — product catalog, editions, features, and SLAs. Defines the authoritative model for entitlements and what downstream systems may provision.
- Billing — subscriptions, pricing models, invoices, usage-based rating, proration, and payments. Publishes entitlement updates and billing events.
- Configuration & Feature Flags — runtime settings and flag evaluations across tenant and edition scopes. Provides evaluation APIs with low-latency cache.
- Usage & Metering — intake and aggregation of usage events. Feeds billing and quota enforcement, ensures fairness, and publishes exceeded quota events.
- Notifications & Extensibility — communication via email/SMS/push and outbound webhooks. Templating, tenant-scoped branding, and signed delivery.
- Audit & Compliance — immutable audit streams, retention policies, data classification, and eDiscovery exports. Cross-cutting enforcement of compliance.
- AI Orchestration — agentic automation flows and AI-assisted tasks. Guardrails, least-privilege tool grants, and PR-based mutation flows.
Collaboration Styles and Relationships¶
The contexts interact using patterns that preserve autonomy:
- Event-first integration — state changes in one context publish events (via outbox) consumed by downstream contexts.
- Conformist downstreams — when latency is critical, downstream contexts adopt the upstream’s published event schema.
- Anti-corruption layers — when interfacing with external services (e.g., payment gateways, third-party identity providers), ACLs translate to/from the platform’s ubiquitous language.
- Published language via APIs — some contexts expose documented APIs for queries or administrative actions, but the default is to consume events.
- Webhooks — for tenant extensibility, the Notifications context delivers signed webhooks that external systems consume.
Upstream / Downstream Dynamics¶
- Tenant Management is upstream to Identity, Core Metadata, and Billing — it establishes the tenant anchor.
- SaaS Core Metadata is upstream to Billing, Config/Flags, and Identity — it defines entitlements and valid editions.
- Billing consumes entitlements from Metadata and usage from Metering; it is downstream to both.
- Configuration & Feature Flags consumes Metadata and tenant overrides; it is downstream to both.
- Usage & Metering consumes flag rules (for quota enforcement) and produces events upstream to Billing.
- Audit & Compliance is cross-cutting: every other context emits audit records to it.
- Notifications & Extensibility is downstream of all contexts that need user-facing communication or webhook projection.
- AI Orchestration consumes read models and events from all other contexts, but never mutates them directly; it operates only via guarded contracts.
Context Ownership and Evolution¶
Each bounded context is aligned with a long-lived team. Teams own code, contracts, events, and SLOs. Ownership is defined as:
- Core Contexts (Tenant, Identity, Metadata, Billing): platform backbone, must be stable, additive evolution only, versioned contracts.
- Supporting Contexts (Config/Flags, Metering, Notifications, Audit): evolve frequently but within backward-compatible rules.
- Innovative Contexts (AI Orchestration): intentionally fast-moving, isolated by guardrails and PR-based integration, so experiments never leak into authoritative contexts.
Contracts evolve under clear rules: additive changes are version bumps within the same stream; breaking changes require new topics or new API paths. Consumers are required to be idempotent and resilient to schema growth.
Multi-Tenancy & Tenant Management Context¶
Context Summary¶
Owns the lifecycle of organizations that consume the platform. Assigns authoritative tenantId, enforces regional residency and isolation, and coordinates first-time provisioning (defaults, edition wiring) without leaking external models into other contexts.
Ubiquitous Language¶
- Tenant: An organizational account with contractual right to use ConnectSoft services.
- Residency: Jurisdictional placement of a tenant’s data and compute (e.g.,
EU,US,IL). May imply storage/account partitioning. - Edition Defaults: The initial feature/flag configuration derived from a chosen product edition, applied at provisioning.
- Lifecycle: The administrative state of a tenant (e.g.,
Provisioning,Active,Suspended,PendingDeletion,Deleted).
Aggregates / Entities / VOs / Enums¶
- Aggregate Root
Tenant- Identity:
TenantId(opaque ULID/GUID string) - State:
LifecycleStatus,TenantProfile,TenantRegionResidency,Contacts[],EditionDefaultsRef,CreatedAtUtc,LockedAtUtc?
- Identity:
- Entities
Contact(Id, Name, Email, Phone, Role:Owner,Billing,Technical)
- Value Objects
TenantProfile(LegalName, DisplayName, BillingAccountRef?, Domain? (optional vanity), Tags[])TenantRegionResidency(RegionCode, DataSiloId, ResidencyLocked: bool)TenantScopedId(TenantId, LocalId) for downstream references
- Enumerations
LifecycleStatus=Provisioning | Active | Suspended | PendingDeletion | DeletedSuspensionReason=Billing | Abuse | AdminRequest | SecurityDeletionReason=Requested | NonPayment | ToSViolation
Commands & Events¶
- Commands (write-side)
ProvisionTenant(tenantProfile, residency, initialEditionRef, contacts[])ActivateTenant(tenantId)UpdateTenantProfile(tenantId, profilePatch)AddOrUpdateContact(tenantId, contact)LockResidency(tenantId)(required beforeActivateTenantif policy demands)SuspendTenant(tenantId, reason)ReinstateTenant(tenantId)RequestTenantDeletion(tenantId, reason)→ transitions toPendingDeletionFinalizeTenantDeletion(tenantId)→Deleted(after data disposal workflow)
- Domain Events (emitted by
Tenant)tenant.createdtenant.updatedtenant.residency_lockedtenant.activatedtenant.suspendedtenant.reinstatedtenant.deletion_requestedtenant.deletedtenant.contact_added/tenant.contact_updatedtenant.edition_defaults_applied
All events include: eventId, occurredAtUtc, tenantId, aggregateVersion, payload, and correlation/causation IDs for orchestration.
Policies / Sagas¶
- Residency Constraints Policy: Prevents activation unless residency is set and, if required by regulation, locked. Emits
tenant.residency_lockedonce locked. - Edition Defaults Provisioning Policy: On
tenant.created, fetches edition defaults from SaaS Core Metadata and applies them, emittingtenant.edition_defaults_applied. Failures trigger compensations and keep tenant inProvisioning. - RLS/Filters Enforcement Policy: Publishes
tenantIdand (optional)DataSiloIdto identity/infra so storage and message paths are tenant-scoped by default. - Deletion Orchestrator Saga: On
tenant.deletion_requested, coordinates: user offboarding, config teardown, billing closeout, data disposal/export window, then issuesFinalizeTenantDeletion.
Repository & Read Model¶
- Repository
ITenantRepository: Load/save byTenantIdonly. Optimistic concurrency via aggregate version.- No cross-tenant queries; repository boundary always requires
tenantId.
- Read Models
TenantDirectory(query-optimized view: status, region, editionRef, key contacts)TenantProvisioningStatus(stepwise progress for operational dashboards)ResidencyMap(tenant → data silos/partitions) for infra/config consumers
Contracts (APIs & Message Endpoints)¶
- HTTP (admin plane)
POST /tenants→ ProvisionTenantPOST /tenants/{id}:activate→ ActivateTenantPOST /tenants/{id}:lock-residency→ LockResidencyPATCH /tenants/{id}→ UpdateTenantProfilePOST /tenants/{id}:suspend/POST /tenants/{id}:reinstateDELETE /tenants/{id}→ RequestTenantDeletionPOST /tenants/{id}:finalize-deletion
- Events (integration plane)
- Topics:
tenants.domain.v1.*(domain events),tenants.ops.v1.*(operational signals) - Outbox → durable bus; at-least-once semantics; consumers must be idempotent
- Topics:
Invariants & State Machine¶
- Identity & Uniqueness
TenantIdis authoritative and immutable.- Optional uniqueness:
TenantProfile.Domainmust be globally unique if used for routing.
- Residency
- Must be set during provisioning; cannot change after
ResidencyLocked=true. - If residency is jurisdiction-critical, locking is mandatory before activation.
- Must be set during provisioning; cannot change after
- Activation
- Requires: residency set (and locked if policy), edition defaults applied successfully, minimum 1
Ownercontact.
- Requires: residency set (and locked if policy), edition defaults applied successfully, minimum 1
- Suspension
- Suspended tenants cannot receive new entitlements or issue new tokens for paid features; background jobs may be curtailed.
- Deletion
PendingDeletionblocks new user sign-ups and de-scopes tokens;Deletedis terminal. No reactivation fromDeleted.
- State Machine
Provisioning → Active → (Suspended ↔ Active) → PendingDeletion → Deleted
Security & Compliance Notes¶
- Authorization
- Admin plane endpoints require platform admin scopes; tenant-scoped updates require
TenantAdminwithin the tenant.
- Admin plane endpoints require platform admin scopes; tenant-scoped updates require
- Data Isolation
- Every persistent and message path carries
tenantId; read/write side enforce tenant scoping by default. Infra consumesResidencyMapto place data in correct silo.
- Every persistent and message path carries
- Audit
- All commands produce audit records (
who,when,why, deltas). Correlate with request/trace IDs.
- All commands produce audit records (
- PII Handling
- Contacts and profile fields are classified; logs/events apply redaction where required.
Evolution & Versioning¶
- Additive first: new optional profile fields and event attributes are backward-compatible.
- Breaking changes create new event topics (
.v2) or new API paths. - Residency catalogs evolve via configuration (not code). Transitions between catalogs never auto-migrate existing tenants—explicit migration workflows are required.
Identity (Authentication, Authorization, Federation)¶
Context Summary¶
Owns identities for people and workloads across tenants. Issues multi-tenant tokens enriched with roles, scopes, and edition-aware entitlements. Provides RBAC and ABAC evaluation, role lifecycle, external federation (OIDC/SAML/SCIM), and service principal management for automation and integrations.
Ubiquitous Language¶
- User: A human identity authenticated via passwordless/OTP, password, or external IdP; belongs to one or more tenants with tenant-local roles.
- Service Principal (SP): A non-human identity (app/integration) with client credentials, tenant-local permissions, and optional environment restrictions.
- Role: A named set of permissions aligned with business capabilities (e.g.,
TenantAdmin,BillingAdmin,SupportAgent,Reader). - Scope: A permission atom exposed on APIs (e.g.,
billing.read,config.write), used for least-privilege tokens. - Entitlements: Effective feature rights derived from Edition and tenant overrides; included as claims for downstream enforcement.
- Federation: Trust relationships with external IdPs (OIDC/SAML) and provisioning via SCIM.
- Policy Decision: Result of evaluating RBAC + ABAC against a request context (actor, tenant, resource, action, attributes).
Aggregates / Entities / VOs / Enums¶
- Aggregate Roots
User- Identity:
UserId - State:
Emails[],PrimaryEmail,Status(Invited|Active|Disabled|Locked),TenantMemberships[], MFA settings, external identities (mapped IdP subjects)
- Identity:
ServicePrincipal- Identity:
ServicePrincipalId - State:
DisplayName,TenantBindings[],Credentials[](client secrets/keys),Status
- Identity:
Role- Identity:
RoleId - State:
Name,Description,Scopes[],AssignableBy[](role hierarchy),IsSystemRole
- Identity:
- Entities
RoleAssignment(Id,Subject= User|SP,TenantId,RoleId,ExpiresAt?)ExternalIdentity(IdP, SubjectId, Attributes snapshot for mapping)
- Value Objects
UserEmail(Address, VerifiedAt?)HashedSecret(Algorithm, Hash, CreatedAt, ExpiresAt?)PolicyContext(ActorId, TenantId, Resource, Action, Attributes)
- Enumerations
Scope(catalog of API operations)Status(for User/SP lifecycle)
Commands & Events¶
- Commands
InviteUser(tenantId, email, roles[])RegisterUser(email, credentials?, externalIdentity?)ActivateUser(userId)/DisableUser(userId)/LockUser(userId)AssignRole(subjectId, tenantId, roleId, expiresAt?)/RevokeRole(...)CreateServicePrincipal(tenantId, name, roles[])RotateServicePrincipalSecret(spId)ConfigureFederation(provider, settings)/EnableSCIM(provider)/SyncSCIMUser(...)IssueToken(subjectId, tenantId, scopes[], audience, ttl)
- Events
user.invited/user.registered/user.activated/user.disabled/user.lockedrole.assigned/role.revokedservice_principal.created/service_principal.secret_rotatedfederation.configured/scim.sync_performedtoken.issued(audit stream; never contains raw secrets)
All events carry eventId, occurredAtUtc, tenantId? (where applicable), subjectId, aggregateVersion, payload, correlationId, causationId.
Policies / Sagas¶
- RBAC Policy: Grants based on
RoleAssignmentsat the tenant scope; role hierarchy controls who can assign what. - ABAC Policy: Evaluates request attributes (edition, entitlements, residency tags, resource owner) alongside RBAC for fine-grained decisions.
- Edition-Aware Claims Policy: On token issuance, pulls effective entitlements from SaaS Core Metadata + Config overrides; embeds as claims.
- Federation & SCIM Policy: Normalizes external identities, maps groups/claims to local roles, and provisions users via SCIM with least-privilege defaults.
- Secret Rotation Policy: Enforces rotation cadence for SP credentials; emits
service_principal.secret_rotated.
Repository & Read Model¶
- Repositories
IUserRepository,IServicePrincipalRepository,IRoleRepository,IRoleAssignmentRepository- Load/save by Id; enforce optimistic concurrency.
- Read Models
SubjectDirectory(user/SP summary by tenant)EffectivePermissions(precomputed scopes/entitlements per subject+tenant)FederationDirectory(providers, mappings, SCIM sync status)
Contracts (APIs & Token Schema)¶
- HTTP (admin plane)
POST /tenants/{tenantId}/users:invitePOST /users:registerPOST /tenants/{tenantId}/roles:assignPOST /tenants/{tenantId}/service-principalsPOST /auth/token(Passwordless/PKCE/Client Credentials)
- Token / Claim Schema (JWT)
sub:UserIdorServicePrincipalIdtid:TenantId(required for tenant-scoped tokens)scp: array ofScope(least-privilege)rol: tenant-local roles (optional)ent: entitlements (feature flags/feature keys effective for this token)edn: edition identifier bound at issue-timeres: residency tag/data silo hint (for downstream enforcement)aud,iss,iat,exp,jti- Notes: No PII in tokens. Entitlements are hashed keys or short codes, not verbose configs.
Invariants & State Machine¶
- A
Usermust have at least one verified email beforeActive. - A
Rolecannot be deleted while assignments exist. - A
ServicePrincipalmust have at least one validHashedSecretto issue tokens. RoleAssignmentis tenant-scoped; cross-tenant roles are separate assignments.- Token issuance requires: subject enabled, tenant exists and not suspended, scopes allowed by roles and policy, audience registered.
User State: Invited → Active ↔ Disabled/Locked
Service Principal State: Created → Active ↔ Disabled
Role Lifecycle: Defined → Published → (Deprecated) → Retired (no removal until zero assignments)
Security & Compliance Notes¶
- Authentication: Supports passwordless (magic link/OTP), WebAuthn, OAuth2/OIDC, and client credentials for SPs.
- Authorization: Evaluate RBAC first, then ABAC constraints. Deny by default; scopes cannot exceed role grants.
- Key Management: Signing keys in HSM/KeyVault; secrets never logged. All sensitive fields use classification/redaction in logs/events.
- Audit: Every admin change and token issuance is audited with correlation to upstream requests.
- Tenant Isolation: Tokens always carry
tid; downstream services must reject missing/foreign tenant contexts.
Federation & SCIM¶
- Providers: OIDC and SAML with explicit mapping configs (claims → roles, groups → roles).
- Just-in-Time (JIT) Provisioning: Optional; creates a disabled user pending admin approval or auto-assigns minimal roles per mapping policy.
- SCIM 2.0: Supports user/group sync; external IDs tracked in
ExternalIdentity. - Deprovisioning: SCIM group removal or IdP disable cascades to revoke roles and tokens.
Evolution & Versioning¶
- Additive claim growth is allowed; breaking token changes introduce a new
iss/audpair or versioned discovery doc. - Role catalogs evolve via migration scripts and deprecation windows.
- Federation mappings versioned per provider; SCIM schema extensions are additive-first.
SaaS Core Metadata Context (Products, Editions, Features, SLAs)¶
Context Summary¶
Authoritative source for product composition: products, editions, features, and SLAs. Defines what capabilities exist, how they bundle into editions, and which entitlements flow to downstream contexts. Aligns with pricing/billing and configuration by publishing a stable catalog and change events without leaking implementation details of other contexts.
Ubiquitous Language¶
- Product: A marketable offering composed of one or more editions (e.g., “ConnectSoft Config Service”).
- Edition: A curated bundle of features and quotas defining rights (e.g.,
Free,Standard,Enterprise). - Feature: A capability exposed to tenants and users (e.g., “Webhooks,” “Advanced Flags,” “SLA: 99.9%”).
- Entitlement: The effective rights derived from Edition + overrides (keys/quotas/limits).
- SLA (Service Level Agreement): Formal commitment of availability/support metrics and remedies.
- Compatibility Rule: Constraint ensuring coherent edition evolution (e.g., Feature X requires Feature Y ≥ v2).
- Deprecation: Timelined withdrawal of features/editions with migration guidance.
Aggregates / Entities / VOs / Enums¶
- Aggregate Roots
Product- Identity:
ProductId - State:
Name,Description,Status,DefaultEditionId?,PublishedVersion,Tags[]
- Identity:
Edition- Identity:
EditionId - State:
ProductId,Name,Description,Status,EditionFeatures[],SlaRef?,Version
- Identity:
Feature- Identity:
FeatureId - State:
Key(VO),Name,Description,Category,Status,DefaultQuota?,Version
- Identity:
ServiceLevelAgreement- Identity:
SlaId - State:
AvailabilityTarget,SupportWindow,ResponseTimes,CreditsPolicy,Version,Status
- Identity:
- Entities
EditionFeature(link entity):FeatureId,Mode(Enabled|Conditional|Preview),Quota?(VO),Constraints[]
- Value Objects
Key(canonical identifier for a feature used by downstream systems)Quota(Dimension, Limit, ResetPeriod)CompatibilityConstraint(RequiresFeatureKey, MinVersion?, MutuallyExclusiveWith?)EntitlementDescriptor(FeatureKey, Mode, Quota?, Version)
- Enumerations
Status=Draft | Published | Deprecated | RetiredResetPeriod=PerRequest | Hour | Day | Month | BillingCycle
Commands & Events¶
- Commands
CreateProduct(name, ...)/PublishProduct(productId, versionNotes?)/DeprecateProduct(productId, timeline)CreateFeature(key, ...)/PublishFeature(featureId)/DeprecateFeature(featureId, timeline)CreateEdition(productId, name, ...)/AddFeatureToEdition(editionId, featureId, mode, quota?)/RemoveFeatureFromEdition(...)AttachSla(editionId, slaId)/UpdateSla(slaId, changes)SetCompatibilityRules(editionId, constraints[])
- Domain Events
product.publishedfeature.publishededition.publishededition.changed(features added/removed/updated)sla.attached|updatedentitlements.changed(materialized entitlement set changed for an edition)feature.deprecated/edition.deprecated/product.deprecated
Events include eventId, occurredAtUtc, aggregateId, version, and snapshots sufficient for downstream projections (no excessive verbosity).
Policies / Sagas¶
- Edition Compatibility Policy: Validates that an edition’s features satisfy all
CompatibilityConstraints before publication; blocks publish otherwise. - Default Feature Pack Policy: When a product sets a
DefaultEditionId, ensure it isPublishedand minimally viable (contains mandated baseline features). - Deprecation Semantics Policy: On deprecation events, emit migration guidance and sunset dates; maintain
Publishedread models until retirement date. - Entitlement Materialization Policy: On any
edition.changedorfeature.published/deprecated, recompute the edition’sEntitlementDescriptor[]and emitentitlements.changedfor subscribers (Identity for claim enrichment, Config/Flags for evaluation defaults, Billing for SKU mapping).
Repository & Read Model¶
- Repositories
IProductRepository,IEditionRepository,IFeatureRepository,ISlaRepository- Enforce optimistic concurrency; only aggregate-root persistence.
- Read Models
CatalogView(Products → Editions → Features, statuses and versions)EntitlementsView(Edition → EntitlementDescriptor[])DeprecationCalendar(timeline per product/feature/edition)SlaMatrix(targets and remedies per edition)
Contracts (APIs & Feeds)¶
- HTTP (admin plane)
POST /products,POST /features,POST /products/{id}/editionsPOST /editions/{id}:publish,POST /features/{id}:publish,POST /products/{id}:publishPOST /editions/{id}/features(add),DELETE /editions/{id}/features/{featureId}(remove)POST /editions/{id}:attach-slaPOST /editions/{id}:set-compatibility
- Query (public plane)
GET /catalog(paged);GET /products/{id};GET /editions/{id};GET /features/{id}GET /editions/{id}/entitlements(stable published view)
- Event Topics
catalog.domain.v1.*(domain events),catalog.entitlements.v1(materialized entitlements)
Invariants & State Machine¶
- A
FeatureKeyis globally unique and immutable after publish. - An
Editioncannot bePublishedif any includedFeatureis notPublishedor violates compatibility. - A
Productcannot bePublishedwithout at least onePublishededition. Retiredobjects may not be referenced by new editions; existing references trigger migration flows.ServiceLevelAgreementversions are immutable; updates create a new version and require explicit re-attachment.
Status Transitions:
Draft → Published → Deprecated → Retired (monotonic; no revert to Draft after Published)
Security & Compliance Notes¶
- Admin plane operations require elevated scopes; changes are fully audited with before/after diffs.
- Feature keys and entitlements are non-PII; nevertheless, event payloads avoid leaking internal rationale—only necessary fields for consumers.
- SLAs may include legal references; store references as metadata, not full legal text in events.
Evolution & Versioning¶
- Additive-first: new features or quotas extend descriptors; consumers must ignore unknown fields.
- Breaking changes: publish a new
Feature.VersionorEdition.Version; emitentitlements.changedwith migration notes. - Sunset timelines are explicit in deprecation events; downstream contexts must honor deadlines in their policy engines.
Billing & Pricing Context (Plans, Subscriptions, Rating)¶
Context Summary¶
Authoritative model for commercial relationships with a tenant: plans/editions mapped to subscriptions, pricing models, rating/charging, invoices, credits, and promotions. Consumes entitlements from Catalog (Core Metadata) and usage from Metering to compute charges. Integrates with external payment providers through an adaptor ACL while preserving platform UL.
Ubiquitous Language¶
- Plan: Commercial packaging that references a
Product/Editionand aPricingModel(e.g., “Enterprise Monthly”). - Subscription: A tenant’s contractual enrollment to a Plan with lifecycle, term, and billing cycle.
- Pricing Model: Rules that determine how charges are computed (flat, tiered usage, per-seat, hybrid).
- Rating: The calculation process producing line items from entitlements, seats, and usage.
- Invoice: A bill containing rated charges, taxes/credits/discounts, and payment status.
- Charge: A captured payment transaction against an invoice balance.
- Promotion: Time-bound discount or credit (coupon, introductory pricing).
- Dunning: Automated recovery process for failed payments (retries, grace periods, suspension).
Aggregates / Entities / VOs / Enums¶
- Aggregate Roots
Subscription- Identity:
SubscriptionId - State:
TenantId,PlanId,EditionRef,Status,StartDate,CurrentPeriod { start, end },Seats?,PromotionRefs[]
- Identity:
PricingModel- Identity:
PricingModelId - State:
Kind(Flat|PerSeat|TieredUsage|Hybrid),Currency,PriceComponents[],TaxesPolicyRef?
- Identity:
BillingRule- Identity:
BillingRuleId - State: Mapping rules from entitlements/usage to billable dimensions; proration policy; overage policy
- Identity:
Invoice- Identity:
InvoiceId - State:
TenantId,SubscriptionId,Period,LineItems[],Subtotal,Taxes[],Adjustments[],Total,Currency,Status,DueDate,PaidAt?
- Identity:
- Entities
LineItem(Description, Quantity, UnitPrice:Money, Amount:Money, Dimension?, Metadata)PromotionApplication(PromotionId, Scope, Amount/Percent, Duration, AppliedFrom, AppliedUntil)
- Value Objects
Money(Amount, Currency, arithmetic with rounding rules)UsageThreshold(Dimension, FreeUnits, OverageRate)ProrationPolicy(Strategy:Daily|Hourly|None)Tier(From, To, UnitPrice)SeatPolicy(MinSeats, MaxSeats?, IncludedSeats?)
- Enumerations
BillingCycle=Monthly | Quarterly | YearlySubscriptionStatus=Trial | Active | PastDue | Suspended | Canceled | ExpiredInvoiceStatus=Draft | Issued | Paid | PastDue | Void | Refunded
Commands & Events¶
- Commands
CreatePlan(productId, editionRef, pricingModelId)StartSubscription(tenantId, planId, seats?, startDate?, trialUntil?)ChangeSubscription(subscriptionId, newPlanId|seatDelta|promotionCode, effectiveAt)CancelSubscription(subscriptionId, when: Now|PeriodEnd)GenerateInvoice(subscriptionId, period)/IssueInvoice(invoiceId)RecordUsage(tenantId, subscriptionId, dimension, quantity, occurredAt)(write-through to Metering or internal buffer when needed)CapturePayment(invoiceId, providerRef?)/RefundPayment(invoiceId, amount?)ApplyPromotion(subscriptionId, promotionCode)
- Events
subscription.started/subscription.activatedsubscription.changed(plan/seats/promo/proration)subscription.canceledsubscription.suspended/subscription.reinstated(dunning outcome)invoice.generated/invoice.issued/invoice.paid/invoice.past_due/invoice.voided/invoice.refundedcharge.captured/charge.failed/charge.refundedentitlements.updated(derived when plan/edition changes impact downstream claims)
All events include eventId, occurredAtUtc, tenantId, subscriptionId|invoiceId, aggregateVersion, and correlation/causation IDs.
Policies / Sagas¶
- Edition-Linked Entitlements Policy: When a subscription starts/changes, resolve
EditionReffrom Catalog and emitentitlements.updatedfor Identity and Config/Flags. - Rating Policy: On
GenerateInvoice, gather usage from Metering for the billing window, applyPricingModelandBillingRule(tiers, thresholds, overage), promotions, and taxes, then produceLineItems. - Proration Policy: On mid-period plan/seat changes, pro-rate charges per
ProrationPolicy(daily/hourly) with credits where applicable. - Promotions Policy: Validate promotion eligibility and duration; apply to eligible components only (e.g., waive overage for first N days).
- Dunning Policy: On
charge.failed, schedule retries with backoff; manage grace period and transition toPastDue/Suspended; notify Notifications context. - Provider Adaptor ACL: Normalize payment operations across gateways (Stripe/Adyen/etc.) without leaking provider semantics into the domain.
Repository & Read Model¶
- Repositories
ISubscriptionRepository,IPricingModelRepository,IBillingRuleRepository,IInvoiceRepository- Optimistic concurrency; aggregates persisted atomically with outbox entries.
- Read Models
AccountBalance(running totals, credits)InvoiceLedger(query-optimized invoices with payment summaries)SubscriptionDirectory(status, plan, seats, next invoice date)PricingCatalog(flattened price components for UI/config)
Contracts (APIs & Message Endpoints)¶
- HTTP (admin/self-serve)
POST /tenants/{tenantId}/subscriptionsPOST /subscriptions/{id}:change(plan/seats/promo)POST /subscriptions/{id}:cancelPOST /subscriptions/{id}:generate-invoicePOST /invoices/{id}:capture/POST /invoices/{id}:refundGET /tenants/{tenantId}/invoices?period=...
- Events
billing.subscriptions.v1.*(domain)billing.invoices.v1.*(invoice lifecycle)billing.payments.v1.*(charges)billing.entitlements.v1(entitlements.updatedfor consumers)
Invariants & State Machine¶
- A
Subscriptionmust reference a validPlan(Product+Edition) andPricingModel. Subscription.Activerequires successful initial rating and (if not trial) a payable invoice or within grace period.Invoice.Totalequals sum ofLineItems+Taxes+Adjustments;Totalcannot be negative unless marked asCredit.- Currency must match across
PricingModel,Invoice, andCharge. - Proration and promotions cannot reduce a line item below zero.
- Subscription lifecycle:
Trial → Active → (PastDue ↔ Active) → Suspended → (Reinstated → Active | Canceled | Expired) - Invoice lifecycle:
Draft → Issued → (Paid | PastDue → (Void|Paid)) → Refunded
Security & Compliance Notes¶
- No PANs/PCI data stored in-domain; provider tokens only. Secrets held in KeyVault/HSM.
- All monetary values use
Moneywith explicit rounding (banker’s rounding where required). - Audit includes rating inputs (usage snapshot hashes, price components) for reproducibility without exposing PII.
- Tax/VAT handling via configurable
TaxesPolicyRef; country rules externalized.
Evolution & Versioning¶
- Additive-first pricing components and dimensions; unknown dimensions ignored by older consumers.
- Breaking changes create new
PricingModelIdversions; existing subscriptions grandfathered until migration. - Provider adaptors are versioned behind ACL; switching providers does not change domain events or contracts.
Configuration & Feature Flags Context¶
Context Summary¶
Owns definition and evaluation of runtime flags and settings across product, edition, and tenant scopes. Provides a deterministic precedence model and low-latency evaluation APIs/SDKs. Integrates with Identity (claims, entitlements) and Catalog (feature keys) to compute effective capabilities without duplicating upstream models.
Ubiquitous Language¶
- Flag: A boolean or multi-variant switch controlling behavior at runtime (e.g.,
webhooks.enabled). - Setting: A typed configuration value (string/number/json/datetime) used by services at runtime.
- Scope: The level at which a rule applies—
Product,Edition,Tenant. - Override: A tenant-scoped mutation that replaces or augments upstream defaults.
- Evaluation Context: Attributes used to resolve a flag/setting (e.g.,
tenantId,edition,entitlements,userId,segment,region). - Kill Switch: A high-priority control that forces a flag off (or safe default) globally or per-scope.
Aggregates / Entities / VOs / Enums¶
- Aggregate Roots
FlagSet- Identity:
FlagSetId(scope+ownerRef) - State:
Flags[],Rules[],KillSwitches[],Version,UpdatedAtUtc
- Identity:
SettingSet- Identity:
SettingSetId(scope+ownerRef) - State:
Settings[](typed),Schemas[](validation),Version,UpdatedAtUtc
- Identity:
- Entities
Override(Id, Target:TenantId, Items[] (Flag or Setting mutations),ActivatedAtUtc,ExpiresAtUtc?)Rule(Targeting predicate, Rollout strategy, Value)
- Value Objects
FlagKey(canonical dotted key, validated)EvaluationContext(TenantId, Edition, Entitlements[], UserId?, Region?, Attributes map)Rollout(Strategy:Fixed|Percentage|List, Seed, Buckets)TypedValue(Type:Bool|String|Number|Json|Datetime, Value)
- Enumerations
Scope=Product | Edition | TenantPrecedence=KillSwitch > Tenant Override > Edition Default > Product Default
Commands & Events¶
- Commands
DefineFlag(scope, ownerRef, key, defaultValue, rules?)DefineSetting(scope, ownerRef, key, typedValue, schema?)ApplyOverride(tenantId, items[], ttl?)RemoveOverride(tenantId, keys[])EnableKillSwitch(key, scope?, ownerRef?, reason, ttl?)DisableKillSwitch(key, scope?, ownerRef?)UpdateRules(scope, ownerRef, rules[])
- Events
flag.changed(definition or rules updated)override.applied/override.removedkillswitch.enabled/killswitch.disabledsetting.changed/schema.changed
Events include eventId, occurredAtUtc, tenantId?, key, version, correlation/causation IDs, and minimal diffs for efficient caches.
Policies / Sagas¶
- Precedence Policy: Evaluator always applies
KillSwitch→Tenant Override→Edition Default→Product Default. Missing keys fall back to safe defaults or schema defaults. - Edition Alignment Policy: On Catalog
entitlements.changed, synchronize edition defaults intoFlagSet/SettingSetso evaluation reflects current editions. - Identity-Enriched Evaluation Policy: If
EvaluationContextcarriesentitlementsorroles, allow rules that target these attributes (e.g., enable preview forTenantAdminonly). - Staleness & TTL Policy: Overrides/kill switches with TTL auto-expire; emit removal events and update caches.
- Change Propagation Policy: Any change publishes to an update stream for SDK streaming and server cache invalidation.
Repository & Read Model¶
- Repositories
IFlagSetRepository,ISettingSetRepository,IOverrideRepository,IKillSwitchRepository- Optimistic concurrency via
Versionand ETags.
- Read Models
EffectiveConfigView(tenantId)— a precomputed projection combining product/edition defaults with overrides and active kill switches.KeyIndex— reverse index byFlagKey/SettingKeyto impacted tenants/editions for fast invalidation.AuditTrail— chronological changes for compliance and rollback assistance.
Evaluation & Caching Model¶
- Server-Side Evaluation (authoritative):
- Inputs:
EvaluationContext - Steps: Resolve precedence chain → apply targeting rules/rollouts → return
TypedValuewithversionandsource. - Deterministic hashing for percentage rollouts using
Seed+ stable subject key (tenantId:userId?).
- Inputs:
- Caching Tiers:
- L1 in-process: per-service memory cache keyed by
tenantId+version. - L2 distributed: shared cache (e.g., Redis) storing
EffectiveConfigViewsnapshots with ETag. - Streaming updates: subscribers invalidate L1 on
flag.changed/override.appliedevents; L2 uses pub/sub for fine-grained key eviction.
- L1 in-process: per-service memory cache keyed by
- Cold Start / Warmup:
- On service boot, prefetch
EffectiveConfigViewfor active tenants; lazy fill on miss with circuit breakers and fallback defaults.
- On service boot, prefetch
Contracts (APIs & Message Endpoints)¶
- HTTP (evaluation)
POST /config/evaluate→{ key, context }→{ value, type, source, version, evaluatedAt }POST /config/evaluate/batch→ bulk keys
- HTTP (admin)
PUT /config/flags/{scope}/{ownerRef}/{key}PUT /config/settings/{scope}/{ownerRef}/{key}POST /config/overrides/{tenantId}POST /config/killswitches/{key}:enable/POST ...:disable
- SDKs
- Server SDKs (C#/Node/Go) supporting cached evaluation, streaming updates, and context helpers.
- Events
config.flags.v1.*,config.overrides.v1.*,config.killswitches.v1.*
Invariants & State Machine¶
FlagKeyis globally unique within its scope and immutable once published.- A
KillSwitchoverrides any rule or override for the same key while active. Overrideentries must pass schema validation forSettingkeys.- Percentage rollouts must produce stable assignment per subject; changing the
Seedintentionally reshuffles cohorts. - Edition defaults cannot reference unknown feature keys; alignment with Catalog is enforced before publish.
Security & Compliance Notes¶
- Evaluation inputs may contain identifiers; classify and avoid logging raw
EvaluationContextunless redacted. - Admin mutations require elevated scopes; all changes are audited with before/after diffs.
- No PII in events; tenant identifiers are included strictly for routing/invalidation.
Evolution & Versioning¶
- Additive-first: new flags/settings and new rule predicates co-exist; older SDKs ignore unknown rule types.
- Breaking schema changes require new keys or versioned namespaces (e.g.,
featureX.v2.enabled). - Deprecations emit warnings and provide migration hints in admin APIs; sunset dates tracked in
AuditTrail.
Usage & Metering Context¶
Context Summary¶
Owns intake, normalization, and aggregation of usage signals across tenants. Produces authoritative counters for real-time quota/rate enforcement and billing handoff. Designed as a high-throughput write path with append-only semantics, anti-tamper protections, and resilient late/duplicate event handling.
Ubiquitous Language¶
- Usage Event: An immutable record of activity against a billable/limited dimension (e.g.,
api_calls,messages_tokens,webhooks_delivered). - Dimension: The named metric being measured; bound to Catalog feature keys and Billing dimensions.
- Window: A rolling or fixed period over which usage is accumulated (e.g., per-minute, per-day, per-billing-cycle).
- Counter: The aggregated usage value for a
{tenantId, dimension, window}. - Quota Policy: Limits and behaviors (soft/hard) for a dimension (e.g., 1M calls/month, burst 100 rps).
- Backfill: Import of historical usage for migration or reconciliation.
Aggregates / Entities / VOs / Enums¶
- Aggregate Roots
UsageMeter- Identity:
{tenantId, dimension} - State:
Counters[]byWindow,LastIngestedAt,ReplayCursor, integrity hashes
- Identity:
QuotaPolicy- Identity:
{dimension, plan|edition} - State:
Window,Limit,Burst?,OveragePolicy?,Enforcement(Soft|Hard),GracePeriod?
- Identity:
- Entities
Counter(WindowId, Value, ResetAtUtc, UpdatedAtUtc)
- Value Objects
Dimension(Key, Unit, Normalization rules)Window(Kind:Fixed|Rolling, Size, Anchor)UsageRecord(OccurredAtUtc, Quantity, Source, IdempotencyKey, Signature?, Metadata)
- Enumerations
Enforcement=Soft | HardOveragePolicy=Bill | Throttle | Block | AlertOnly
Commands & Events¶
- Commands
RecordUsage(tenantId, dimension, quantity, occurredAtUtc, idempotencyKey, metadata?)BackfillUsage(tenantId, dimension, records[])(privileged)DefineQuotaPolicy(dimension, planOrEdition, limit, window, enforcement, overagePolicy?)ResetWindow(tenantId, dimension, windowId)(scheduled for fixed windows)
- Events
usage.recorded(normalized, deduped)usage.aggregated(counter updated)quota.nearing_limit(threshold crossed, e.g., 80%)quota.exceeded(limit crossed)usage.backfilled(reconciliation outcome)
All events carry eventId, occurredAtUtc, tenantId, dimension, windowId?, quantity|counterValue, and correlation/causation IDs.
Policies / Sagas¶
- Edition-Aware Limits Policy: When Catalog/Config emits
entitlements.changed, recompute effectiveQuotaPolicyper tenant+dimension. - Anti-Tamper Policy: Validate source signatures (when present), enforce monotonic counters, and reject negative or impossible deltas outside backfill mode.
- Late Event Handling Policy: Accept late usage within a tolerant window; adjust the correct historical counter and emit compensating
usage.aggregated. Very late events route to reconciliation. - Idempotency Policy: Deduplicate by
(tenantId, dimension, idempotencyKey); safe to retry at-least-once deliveries. - Threshold Notification Policy: Emit
quota.nearing_limitat configured thresholds; notify Notifications context and expose advisory headers in evaluation APIs. - Overage/Enforcement Policy: On
quota.exceeded, applyOveragePolicy(emit billable overage, throttle, or block via Config/Identity integration signals).
Repository & Read Model¶
- Write Path
- Append-only usage log (partitioned by
tenantIdanddimension), persisted with an outbox for downstream aggregation.
- Append-only usage log (partitioned by
- Repositories
IUsageMeterRepository,IQuotaPolicyRepository
- Read Models
RealTimeCounters(hot cache of{tenantId, dimension, currentWindows}for enforcement)BillingWindowTotals(period totals aligned with billing cycles)UsageTimeline(time-bucketed series for analytics/dashboards)
Contracts (APIs & Streams)¶
- HTTP (ingest)
POST /metering/usage→ accepts single/batchUsageRecordwith idempotency supportPOST /metering/backfill(privileged)
- HTTP (query)
GET /metering/counters?tenantId=&dimension=&window=GET /metering/limits?tenantId=(effectiveQuotaPolicyview)
- Streams
metering.usage.v1(post-normalization)metering.counters.v1(aggregations)metering.quota.v1(nearing_limit,exceeded)
Ingestion & Aggregation Model¶
- Normalization: Convert provider-specific metrics to canonical
Dimensionunits; reject unknown dimensions. - Bucketing: Fixed windows (e.g., day, month) use anchored buckets; rolling windows maintain sliding aggregates with decay queues or ring buffers.
- Performance: Batched aggregation with atomic counter updates; L1 in-memory + L2 distributed caches.
- Reconciliation: Periodic job compares append-only log to counters; repairs drift and re-emits
usage.aggregatedif needed.
Invariants & State Machine¶
- Counters are non-decreasing within an open window; reset only when the window elapses or during authorized backfill correction.
- Each
UsageRecordmust have a non-emptyidempotencyKeyin online ingest paths. - Dimensions used in quotas must exist in Catalog/Config mapping.
QuotaPolicymust specify aWindow; enforcement without a window is invalid.
Security & Compliance Notes¶
- Ingest endpoints rate-limited and require scoped credentials; sensitive metadata is classified and redacted in logs.
- Multi-tenant scoping enforced on write and read; no cross-tenant queries without platform-admin scope.
- Audit trail of all ingested records and adjustments with hashes for integrity verification.
Evolution & Versioning¶
- New dimensions added additively with mapping tables; old consumers ignore unknown dimensions.
- Changes to window definitions are versioned; overlapping windows may coexist during migration.
- Backfill and reconciliation tools are versioned separately and run in controlled maintenance windows.
Audit & Compliance Context¶
Context Summary¶
Owns the immutable, append-only record of actor/resource changes, administrative operations, policy decisions (authz, data access), and system events across the platform. Provides queryable views, retention enforcement, legal hold, and eDiscovery/export workflows. Applies classification and redaction at write and read time to minimize exposure of sensitive data while preserving investigative value and regulatory traceability.
Ubiquitous Language¶
- Audit Stream: An append-only ledger of
AuditRecordentries partitioned by tenant and category. - Audit Record: A fact describing “who did what to which resource, when, from where,” including decision context (allow/deny), deltas, and correlation IDs.
- Data Class: The sensitivity level of fields involved (e.g.,
Public,Internal,Confidential,PII,Secret). - Retention Policy: Rules governing how long records persist before disposal and exceptions (e.g., legal hold).
- Legal Hold: A suspension of deletion to preserve evidence for investigation or litigation.
- WORM Storage: Write-Once-Read-Many semantics to ensure immutability within the retention window.
Aggregates / Entities / VOs / Enums¶
- Aggregate Root
AuditStream- Identity:
{tenantId, streamId}(streams by category:authz,admin,config,billing,data_access, etc.) - State:
RetentionPolicy,LegalHolds[],LastSequence,IntegrityAnchor(hash chain root)
- Identity:
- Entities
AuditRecord- Fields:
sequence,occurredAtUtc,actor(subject, type, tenant scope),action,resource(type, id),decision(if policy),result(Success|Failure|Denied),deltas?,ip|userAgent?,dataClasses[],redactionHints[],correlationId,causationId,hash,prevHash
- Fields:
- Value Objects
RetentionPolicy(Duration, GraceDays, DeletionMode:Scheduled|Manual, ExportBeforeDelete: bool)ClassificationRule(FieldPath, Class:DataClass, Redaction:Mask|Erase|Hash|None)ExportDescriptor(Format:Parquet|NDJSON|CSV, Range, Filters, Manifest)
- Enumerations
DataClass=Public | Internal | Confidential | PII | SecretDecision=Allow | Deny | NotApplicable | Indeterminate
Commands & Events¶
- Commands
AppendAuditRecord(streamId, record)(only way to write)ConfigureRetention(streamId, retentionPolicy)PlaceLegalHold(streamId, reason, placedBy, until?)ReleaseLegalHold(streamId, reason, releasedBy)StartExport(streamId, descriptor)/FinalizeExport(exportId)PurgeByRetention(streamId, cutoffDate)(system-initiated)
- Events
audit.record_appendedaudit.retention_configuredaudit.legal_hold_placed/audit.legal_hold_releasedaudit.export_started/audit.export_finalizedaudit.purged_by_retention
All entries/events include tenant scoping, sequence, and integrity metadata (hash, prevHash) to enable tamper-evidence.
Policies / Sagas¶
- Retention Enforcement Policy: Enforces default 7-year retention (tenant-configurable within allowed bounds). Schedules purge tasks that respect legal holds and export-before-delete settings.
- PII Minimization & Redaction Policy: Applies
ClassificationRules at append time; sensitive fields are masked/hashed/erased in storeddeltasandmetadatawhile retaining enough context for forensics. Read-time redaction layers ensure only authorized viewers see minimally necessary data. - Decision Logging Policy: When Identity/Config/Billing make allow/deny decisions, an
AuditRecordis appended with the evaluated attributes and outcome (never raw secrets). - Export Packaging Policy: Builds verifiable export bundles (manifest + checksums + optional encryption) for regulators and investigations; supports pagination and resumable exports.
- Integrity Chain Policy: Maintains a per-stream hash chain; periodically publishes an
IntegrityAnchorsnapshot for external timestamping to strengthen non-repudiation.
Repository & Read Model¶
- Write Path
- Append-only ledger optimized for sequential writes; WORM-capable storage tier with time-based partitioning by tenant and month.
- Repositories
IAuditStreamRepositoryfor metadata/anchorsIAuditAppenderfor atomic appends with hash-chain updates
- Read Models
AuditTimeline(time-ordered query with filters: actor, action, resource, decision, data class)PolicyDecisionLog(subset optimized for authorization/compliance analytics)ExportCatalog(history of exports, manifests, checksums, recipients)
Contracts (APIs & Access Patterns)¶
- HTTP (append)
POST /audit/streams/{streamId}/records(internal only; authenticated service principals)
- HTTP (query)
GET /audit/streams/{streamId}/records?from=...&to=...&actor=...&action=...&resource=...&decision=...&class=...GET /audit/streams/{streamId}/integrity(returns anchors and verification instructions)
- HTTP (admin)
PUT /audit/streams/{streamId}/retentionPOST /audit/streams/{streamId}:legal-holdDELETE /audit/streams/{streamId}:legal-holdPOST /audit/streams/{streamId}:export
- Streams
audit.events.v1.*for meta-events (holds, retention changes, exports)
Invariants & State Machine¶
AuditRecord.sequenceis strictly increasing within a stream; gaps are invalid.hash = H(record || prevHash); changing any stored record invalidates chain verification.- Retention purges never run on streams with active legal holds.
- Append operations must include
correlationIdfor cross-context traceability. - Classification must be resolved before persist; records lacking classification are rejected.
Security & Compliance Notes¶
- Access to query/export is role-gated (
ComplianceOfficer,SecurityAnalyst,TenantAdmin (limited)); fine-grained ABAC filters enforce tenant and purpose restrictions. - Exports can be encrypted with customer-managed keys; access is logged and watermarked.
- No raw credentials, tokens, or PANs may be stored; use one-way hashes or redacted placeholders.
- DSAR support: queries can be filtered by data subject identifiers to facilitate subject access and deletion (where legally permitted, not overriding audit retention obligations).
Evolution & Versioning¶
- Schema evolves additively; new fields default to redacted unless classified.
- Retention defaults may change across policy versions; each stream records the
RetentionPolicy.Version. - Integrity scheme upgrades (e.g., switching hash algorithms) are versioned; anchors include algorithm metadata with migration procedures.
Notifications & Extensibility Context (Email/SMS/Webhooks)¶
Context Summary¶
Provides tenant-scoped communications (email/SMS/push) and outbound extensibility via signed webhooks. Normalizes provider differences behind an adaptor ACL, enforces branding and rate limits, and guarantees at-least-once delivery with idempotent receivers. Integrates with other contexts to announce lifecycle events, billing changes, quota thresholds, and administrative actions.
Ubiquitous Language¶
- Notification Template: Parameterized content with localization, channels, and tenant branding.
- Channel Config: Tenant’s provider settings and policies per channel (email, SMS, push).
- Dispatch: A request to send a message to one or more recipients with a template and data model.
- Delivery Status: The state machine for a message (
Queued,Sent,Delivered,Bounced,Failed). - Webhook Endpoint: A tenant-registered HTTP callback target with signing configuration and replay protection.
- Preference: Recipient’s subscription/consent for categories (e.g., billing, system alerts, marketing).
Aggregates / Entities / VOs / Enums¶
-
Aggregate Roots
-
NotificationTemplate- Identity:
TemplateId - State:
Name,ChannelKinds[],Localization[],Category,Placeholders[],Version,Status -
ChannelConfig -
Identity:
{tenantId, channelKind} - State:
ProviderRef,CredentialsRef,RateLimit,CategoryPolicies[],DefaultFrom/Branding -
WebhookEndpoint -
Identity:
EndpointId - State:
TenantId,Url,Secret(s),SignatureSpec(algo, header names),TimestampsTolerance,Enabled,ReplayWindow - Entities
- Identity:
-
DispatchRequest(Id,TemplateId,TenantId,Recipients[],Data,Category,CorrelationId) Message(Id, Channel, RenderedBody, Headers,DeliveryStatus, ProviderMessageId?, Attempts, LastError?)SecretVersion(KeyId, CreatedAt, ExpiresAt?, State)-
Value Objects
-
Branding(LogoUrl, Colors, Footer, FromName/Email/SMS SenderId, Domain) RateLimit(Window, Max)Signature(Algorithm:HMAC-SHA256|Ed25519, KeyId)-
Enumerations
-
DeliveryStatus=Queued | Sent | Delivered | Bounced | Failed | Suppressed
Commands & Events¶
-
Commands
-
CreateTemplate(...)/PublishTemplate(templateId) ConfigureChannel(tenantId, channelKind, config)Send(dispatchRequest)(may be batch)Retry(messageId)(policy-driven backoff)RegisterWebhook(tenantId, url, spec)/RotateWebhookSecret(endpointId)/DisableWebhook(endpointId)RecordPreference(subjectId, category, allowed)-
Events
-
message.queued/message.sent/message.delivered/message.bounced/message.failed/message.suppressed webhook.signed/webhook.delivered/webhook.failedtemplate.published/channel.configured/webhook.registered|disabled|rotatedpreference.changed
Events carry tenantId, messageId|endpointId, category, correlationId, and provider metadata (minimal, non-PII).
Policies / Sagas¶
- Tenant Branding Policy: On
Send, merge tenantBrandingwith template; ensure DMARC/SPF alignment for email domains where applicable. - Rate Limit Policy: Enforce
RateLimitper tenant+channel; queue overflow with backoff; emitmessage.suppressedif policy requires. - Provider Adaptor ACL: Abstract provider specifics (SendGrid/Mailgun/Twilio/FCM/etc.). Normalize errors and receipts; never leak provider tokens in events.
- Preference & Compliance Policy: Honor user/category preferences and legal constraints (CAN-SPAM/GDPR). Marketing categories require explicit consent; system-critical may bypass with audit.
- Retry & DLQ Policy: Exponential backoff; transition to DLQ after N attempts; surface DLQ metrics for ops. Idempotent resend protected by
messageId. - Webhook Signature & Replay Policy: Sign payload with
KeyId+Signature; includetimestampandnonce. Receivers must validate HMAC/Ed25519, check freshness withinTimestampsTolerance, and dedupe bynonce.
Repository & Read Model¶
-
Repositories
-
ITemplateRepository,IChannelConfigRepository,IWebhookRepository,IDispatchRepository,IMessageRepository,IPreferenceRepository -
Read Models
-
MessageOutbox(pending sends and attempts) DeliveryLog(status timelines per message/recipient)WebhookDeliveryLog(attempts, signatures, latencies)PreferenceDirectory(effective opt-ins per subject/category)
Contracts (APIs & Message Endpoints)¶
-
HTTP (admin)
-
POST /tenants/{tenantId}/templates/POST /templates/{id}:publish PUT /tenants/{tenantId}/channels/{channelKind}POST /tenants/{tenantId}/webhooks/POST /webhooks/{id}:rotate/POST /webhooks/{id}:disablePUT /preferences/{subjectId}/{category}(consent)-
HTTP (send)
-
POST /notifications:send→ acceptsDispatchRequest(batch supported) -
Events to external receivers (webhooks)
-
Headers:
X-CS-Signature,X-CS-KeyId,X-CS-Timestamp,X-CS-Nonce - Body: event name, version, tenant, payload,
correlationId -
Inbound Provider Webhooks
-
Provider-specific endpoints behind ACL for delivery/bounce receipts, mapped to
message.*events
Invariants & State Machine¶
- A
DispatchRequestmust reference aPublishedtemplate and a configured channel. - Messages are immutable after
Sent; onlyDeliveryStatustransitions change. - Webhook endpoints require at least one active
SecretVersion; rotation retains previous key for overlap window. - Replay windows reject duplicate
(endpointId, nonce)withinReplayWindow. - Suppressed messages (due to preferences or policy) never enter provider pipelines.
Message lifecycle: Queued → Sent → (Delivered | Bounced | Failed) with policy-driven retries.
Webhook lifecycle: Signed → Delivered | Failed with retries until max attempts/DLQ.
Security & Compliance Notes¶
- Secrets and provider credentials stored in HSM/KeyVault; access by least privilege.
- PII minimization: templates support placeholder filtering/redaction; logs store only necessary metadata.
- All sends and preference changes are audited with correlation to upstream requests.
- Link tracking (if enabled) uses privacy-preserving redirects and respects Do-Not-Track/preferences.
Evolution & Versioning¶
- Templates are versioned; changes require
PublishTemplateto take effect. - Webhook signature specs are versioned per endpoint; receivers can rotate to new schemes without downtime.
- New channels/providers added behind ACL without impacting domain contracts.
AI Orchestration Context (Optional but First-Class)¶
Context Summary¶
Provides agentic workflows that read from authoritative domains, synthesize artifacts (docs, PRs, scripts), and propose changes via controlled interfaces. Never writes directly to domain aggregates. All mutations occur through PR-based processes, with explicit human or policy-based approval. Ensures least-privilege tool grants, full traceability, and safe rollback.
Ubiquitous Language¶
- Automation Flow: A structured, auditable sequence of agent steps that accomplish a goal (e.g., “Generate ADR + PR for a new quota policy”).
- Artifact: A concrete output produced by a step (markdown spec, migration script, test suite, PR diff).
- Tool: A callable capability (read-only domain query, doc generator, code scaffold, test executor, PR creator).
- Tool Grant: A scoped, time-limited permission for a flow to call specific tools with bounded inputs/tenants.
- Guardrail Policy: Constraints defining what the flow may read, generate, and propose; includes reviewers, CI checks, and redaction rules.
- Run Log: An append-only trace of prompts, tool calls (parameters/outputs hashes), decisions, and artifacts.
Aggregates / Entities / VOs / Enums¶
- Aggregate Roots
AutomationFlow- Identity:
FlowId - State:
Goal,Initiator(human/system),Status(Planned|Running|AwaitingReview|Completed|Aborted|Failed),ToolGrants[],GuardrailPolicyRef,RunLogRef,Artifacts[]
- Identity:
ArtifactBundle- Identity:
ArtifactBundleId - State:
Artifacts[](typed),Checks[](lint/tests/security),ProposedPR?,IntegrityHash,Provenance
- Identity:
- Entities
RunLog(ordered entries with timestamps, step kind, tool call digests, decision notes, redaction markers)ProposedPR(Repo, Branch, DiffSummary, Linked Work Items, Reviewers, Status)
- Value Objects
ToolGrant(ToolId, AllowedOperations, Scope: tenants/contexts, InputSizeLimits, ExpiresAt)GuardrailPolicy(RequiredChecks, ReviewerPolicy, SensitiveDataRules, AllowedRepos, AllowedBranches, MaxChangeSize, PRTitleTemplate)ArtifactRef(Type:ADR|HLD|Code|Migration|Test|Dataset, URI, SHA256)
- Enumerations
FlowStatus=Planned | Running | AwaitingReview | Completed | Aborted | FailedArtifactType=ADR | HLD | Code | Migration | Test | Dataset | Diagram
Commands & Events¶
- Commands
StartFlow(goal, initiator, policyRef, initialGrants[])GrantTool(flowId, toolGrant)/RevokeTool(flowId, toolId)ProduceArtifact(flowId, artifactType, contentRef)(write to ArtifactStore; record digest)OpenProposedPR(flowId, artifactBundleId)/UpdateProposedPR(flowId, diffRef)RequestReview(flowId)/AbortFlow(flowId, reason)/CompleteFlow(flowId)
- Events
flow.startedartifact.generatedtool.granted/tool.revokedpr.opened/pr.updated/pr.checks_passed/pr.checks_failed/pr.merged/pr.closedflow.completed/flow.aborted/flow.failed
All events include flowId, initiator, policyVersion, artifact digests where applicable, and correlation IDs to external CI systems.
Policies / Sagas¶
- Least-Privilege Tool Use: A flow cannot call a tool without an active
ToolGrant. Grants expire and are scoped to tenants/contexts. Requests for escalation are routed to reviewers with rationale. - Auditable Traces: Every prompt, tool input/output digest, and artifact hash is recorded in
RunLog. Sensitive content is redacted perGuardrailPolicy. - PR-Only Mutations: Any proposed domain change must be expressed as a PR. Direct writes to aggregates are forbidden; CI checks (lint/tests/security/architecture rules) gate merge.
- Reviewer & Check Gate:
RequestReviewenforces reviewer quorum and checklists (e.g., privacy review for telemetry additions). Automatic merges are allowed only if policy permits. - Rollbacks & Safety: On
pr.checks_failedorflow.failed, the saga rolls back ephemeral resources (branches, temp environments) and notifies owners. - Artifact Quality Policy: Generated artifacts must pass format validators (ADR schema, HLD lint, code style). Failing artifacts are rejected from bundling.
Repository & Read Model¶
- Repositories
IAutomationFlowRepository,IArtifactBundleRepository,IRunLogRepository,IToolGrantRepository,IGuardrailPolicyRepository
- Read Models
FlowDashboard(status, owners, outstanding reviews, age)ArtifactIndex(by tenant/context/type; deduplicated via hash)PolicyMatrix(who can grant which tools; default timeouts)PRStatusBoard(per repo/branch: checks, reviewers, merge SLA)
Contracts (APIs & Integrations)¶
- HTTP (orchestration)
POST /ai/flows(start)POST /ai/flows/{id}:grant-toolPOST /ai/flows/{id}:produce-artifactPOST /ai/flows/{id}:open-prPOST /ai/flows/{id}:request-reviewPOST /ai/flows/{id}:abort/POST /ai/flows/{id}:complete
- Tooling Interfaces
- Read-only domain adapters: Catalog, Config, Billing, Metering, Audit (query endpoints only)
- DevOps adapters: Git (repos/branches/PRs), CI (pipelines/results), Issue Trackers (work items)
- Document adapters: ADR/HLD repositories with schema validators
- Event Topics
ai.flows.v1.*,ai.artifacts.v1.*,ai.prs.v1.*
Invariants & State Machine¶
- A flow cannot transition to
AwaitingReviewwithout at least oneArtifactBundleorProposedPR. ToolGrant.ExpiresAtmust be in the future at use time; expired grants cause the step to fail closed.- Artifact digests are immutable; any modification creates a new artifact entry and updates the bundle.
- Flows that open PRs must attach
GuardrailPolicysnapshot and CI configuration IDs to ensure reproducible checks. - Lifecycle:
Planned → Running → (AwaitingReview → Completed | Failed | Aborted)
Security & Compliance Notes¶
- Tools operate under service principals with scoped tokens; no human tokens are stored.
- Redaction on prompts/outputs per
GuardrailPolicywith classification tags; RunLog stores hashes for large outputs and retains only necessary snippets. - All PRs reference work items and include a minimal data-exposure assessment if changes touch logging/telemetry or PII-bearing paths.
- Egress controls: flows cannot call arbitrary endpoints; only allowlisted adapters and repositories.
Evolution & Versioning¶
GuardrailPolicyis versioned; each flow records the policy version applied.- Tool interfaces are additive-first; breaking changes require a new ToolId/version and migration helpers.
- PR templates and check suites are referenced by versioned IDs to ensure stable historical verification.
Patterns Cookbook & Reference Implementations¶
Aggregate Template¶
// Domain: Shared kernel
public abstract class AggregateRoot<TId>
{
private readonly List<IDomainEvent> _uncommitted = new();
public TId Id { get; protected set; } = default!;
public int Version { get; private set; } = 0;
protected void Ensure(bool condition, string message)
{
if (!condition) throw new DomainRuleViolationException(message);
}
protected void Emit(IDomainEvent @event)
{
Apply(@event);
_uncommitted.Add(@event);
Version++;
}
protected abstract void Apply(IDomainEvent @event);
public IReadOnlyCollection<IDomainEvent> DequeueUncommitted() {
var copy = _uncommitted.ToArray();
_uncommitted.Clear();
return copy;
}
}
public sealed class DomainRuleViolationException : Exception
{
public DomainRuleViolationException(string message) : base(message) { }
}
Usage pattern
public sealed class Subscription : AggregateRoot<string>
{
public string TenantId { get; private set; } = default!;
public string PlanId { get; private set; } = default!;
public SubscriptionStatus Status { get; private set; } = SubscriptionStatus.Trial;
private Subscription() { }
public static Subscription Start(string id, string tenantId, string planId)
{
var agg = new Subscription();
agg.Emit(new SubscriptionStarted(id, tenantId, planId));
return agg;
}
protected override void Apply(IDomainEvent e)
{
switch (e)
{
case SubscriptionStarted s:
Id = s.SubscriptionId;
TenantId = s.TenantId;
PlanId = s.PlanId;
Status = SubscriptionStatus.Trial;
break;
case SubscriptionActivated:
Status = SubscriptionStatus.Active;
break;
case SubscriptionCanceled:
Status = SubscriptionStatus.Canceled;
break;
}
}
public void Activate()
{
Ensure(Status is SubscriptionStatus.Trial or SubscriptionStatus.PastDue,
"Only trial or past-due subscriptions can be activated.");
Emit(new SubscriptionActivated(Id));
}
public void Cancel()
{
Ensure(Status is not SubscriptionStatus.Canceled, "Already canceled.");
Emit(new SubscriptionCanceled(Id));
}
}
Value Objects¶
public abstract record ValueObject;
public sealed record Money(decimal Amount, string Currency) : ValueObject
{
public Money Add(Money other)
{
if (Currency != other.Currency) throw new InvalidOperationException("Currency mismatch.");
return this with { Amount = Amount + other.Amount };
}
}
Smart Enumerations¶
public abstract class SmartEnum<T> where T : SmartEnum<T>
{
public string Name { get; }
public int Value { get; }
protected SmartEnum(string name, int value) { Name = name; Value = value; }
public override string ToString() => Name;
private static readonly Dictionary<int, T> _byValue = new();
private static readonly Dictionary<string, T> _byName = new(StringComparer.OrdinalIgnoreCase);
protected static void Register(T item)
{
_byValue[item.Value] = item;
_byName[item.Name] = item;
}
public static T FromName(string name) => _byName[name];
public static T FromValue(int value) => _byValue[value];
}
public sealed class BillingCycle : SmartEnum<BillingCycle>
{
public static readonly BillingCycle Monthly = new(nameof(Monthly), 1);
public static readonly BillingCycle Quarterly = new(nameof(Quarterly), 3);
public static readonly BillingCycle Yearly = new(nameof(Yearly), 12);
private BillingCycle(string name, int value) : base(name, value) => Register(this);
}
Domain Events & Versioning¶
public interface IDomainEvent
{
Guid EventId { get; }
DateTime OccurredAtUtc { get; }
string AggregateId { get; }
int AggregateVersion { get; }
string EventType { get; } // e.g., "subscription.started"
string SchemaVersion { get; } // e.g., "v1"
}
public abstract record DomainEventBase(
Guid EventId,
DateTime OccurredAtUtc,
string AggregateId,
int AggregateVersion,
string EventType,
string SchemaVersion = "v1") : IDomainEvent;
public sealed record SubscriptionStarted(string SubscriptionId, string TenantId, string PlanId)
: DomainEventBase(Guid.NewGuid(), DateTime.UtcNow, SubscriptionId, 0, "subscription.started", "v1");
public sealed record SubscriptionActivated(string SubscriptionId)
: DomainEventBase(Guid.NewGuid(), DateTime.UtcNow, SubscriptionId, 0, "subscription.activated", "v1");
public sealed record SubscriptionCanceled(string SubscriptionId)
: DomainEventBase(Guid.NewGuid(), DateTime.UtcNow, SubscriptionId, 0, "subscription.canceled", "v1");
Event versioning guidance
- Prefer additive schema evolution inside the same
SchemaVersion. - Breaking changes → publish a new
SchemaVersionand/or new event type suffix (e.g.,.v2topic). - Consumers must be idempotent and ignore unknown fields.
Outbox / Inbox (Idempotency, Retries)¶
// Infrastructure sketch
public sealed class OutboxMessage
{
public long Id { get; init; }
public string AggregateId { get; init; } = default!;
public string Topic { get; init; } = default!;
public string PayloadJson { get; init; } = default!;
public DateTime OccurredAtUtc { get; init; }
public int Attempt { get; set; }
public string? LastError { get; set; }
}
public interface IOutbox
{
Task EnqueueAsync(IEnumerable<IDomainEvent> events, CancellationToken ct);
Task<IReadOnlyList<OutboxMessage>> DequeueBatchAsync(int max, CancellationToken ct);
Task MarkSucceededAsync(long id, CancellationToken ct);
Task MarkFailedAsync(long id, string error, CancellationToken ct);
}
// Inbox side
public sealed class InboxDeduper
{
private readonly IInboxStore _store; // persists (consumerGroup, messageId) → processedAt
public async Task<bool> TryBeginAsync(string consumerGroup, string messageId, CancellationToken ct)
{
// Returns false when duplicate
return await _store.TryInsertAsync(consumerGroup, messageId, DateTime.UtcNow, ct);
}
}
Retry strategy
- Exponential backoff with jitter (
base^attempt + random(0..delta)). - DLQ after N attempts; DLQ consumers must be manual/ops-driven.
Repositories & Specifications¶
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
IReadOnlyList<(Expression<Func<T, object>> KeySelector, bool Descending)> OrderBy { get; }
int? Skip { get; }
int? Take { get; }
}
public sealed class Specification<T> : ISpecification<T>
{
public Expression<Func<T, bool>> Criteria { get; init; } = _ => true;
public List<(Expression<Func<T, object>> KeySelector, bool Descending)> OrderByInternal { get; } = new();
public int? Skip { get; init; }
public int? Take { get; init; }
public IReadOnlyList<(Expression<Func<T, object>> KeySelector, bool Descending)> OrderBy => OrderByInternal;
}
public interface IRepository<TAgg, TId> where TAgg : AggregateRoot<TId>
{
Task<TAgg?> GetAsync(TId id, CancellationToken ct);
Task SaveAsync(TAgg aggregate, CancellationToken ct); // atomic with outbox
}
public interface IReadRepository<T, TDto>
{
Task<PagedResult<TDto>> QueryAsync(ISpecification<T> spec, CancellationToken ct);
}
public sealed record PagedResult<T>(IReadOnlyList<T> Items, int Total, int Page, int PageSize);
Query whitelisting
- Specifications are composed from approved predicates and projections.
- Deny arbitrary dynamic fields; map API query params → predefined
ISpecification<T>builders.
Consistency Rules¶
Transactional boundaries
- A single command should mutate one aggregate. Cross-aggregate work uses a saga/process manager.
- Persist aggregate and outbox in the same transaction to ensure exactly-once publication semantics (with at-least-once delivery).
Eventual consistency guides
- Downstream read models rebuild from events; consumers must be idempotent.
- Use inbox dedupe on every consumer group.
- Prefer compensations over distributed transactions.
Idempotency keys
- Command layer accepts an optional
IdempotencyKey; store on aggregate to detect replays. - Ingest paths (e.g., usage events) require keys and reject duplicates.
Mapping to Platform Enums (reference)¶
- Access/Scope →
Scope(Identity): e.g.,billing.read,config.write. - Limit/Reset →
QuotaPolicy.ResetPeriod&Window(Metering):Hour|Day|BillingCycle. - BillingCycle/PricingType →
BillingCyclesmart enum (Monthly/Quarterly/Yearly),PricingModel.Kind(Flat/PerSeat/TieredUsage/Hybrid).
Testing Templates (brief)¶
[TestClass]
public class SubscriptionTests
{
[TestMethod]
public void Activate_FromTrial_EmitsSubscriptionActivated()
{
var s = Subscription.Start("sub-1", "t-1", "plan-1");
s.Activate();
var events = s.DequeueUncommitted().ToList();
Assert.IsTrue(events.Any(e => e.EventType == "subscription.activated"));
Assert.AreEqual(SubscriptionStatus.Active, s.Status);
}
}
Observability Hooks¶
- Enrich domain logs with
tenantId,aggregateId,eventType,version,correlationId. - Emit metrics:
domain_events_published_total,outbox_attempts_total,inbox_duplicates_total,saga_compensations_total.
Migration & Backfills¶
- Provide event replay harnesses to rebuild read models.
- Maintain snapshot support for large aggregates (versioned snapshot every N events).
Conclusion & Summary¶
This blueprint distills the ConnectSoft SaaS Platform into clear, ownable domains with explicit contracts. Each bounded context speaks its own ubiquitous language, enforces its invariants through aggregates, and communicates through well-versioned events and APIs. Together, they form a factory-grade foundation that is safe to evolve, straightforward to operate, and ready for AI-assisted delivery—without sacrificing isolation or compliance.
What this blueprint gives you¶
- A strategic map of capabilities and context relationships that prevents model leakage and team thrash.
- Tactical building blocks—aggregates, entities/VOs, smart enums, repositories/specifications, events, outbox/inbox—that you can lift into code as-is.
- Contract-first integration patterns (event-first, ACLs, webhooks) ensuring interoperability without coupling.
- Governance by design via versioning rules, invariants, auditability, and security/compliance hooks.
Pillars that anchor the platform¶
- Multi-tenancy & residency: Tenant identity and data placement are first-class, enforced at storage and message boundaries.
- Identity & authorization: RBAC + ABAC with edition-aware entitlements in tokens; federation/SCIM without leaking external models.
- Catalog truth: Products, editions, features, SLAs, and entitlements are authoritative and drive downstream behavior.
- Commerce correctness: Subscriptions, pricing, rating, invoices, and dunning align with usage and entitlements, provider-agnostic behind an ACL.
- Runtime control: Flags/settings with deterministic precedence, kill switches, and low-latency evaluation with cache coherency.
- Usage you can trust: High-throughput intake, anti-tamper aggregation, quota signals, and billing handoff.
- Compliance everywhere: Immutable audit streams, retention/legal hold, redaction/minimization from write to read.
- Extensibility: Templated comms and signed webhooks with rate limits, preferences, and replay protection.
- AI with guardrails: Agentic flows that only propose changes via PRs, with least-privilege tool grants and complete provenance.
Cross-context contract highlights (at a glance)¶
- Tenant → upstream for Identity, Catalog, Billing; emits lifecycle events consumed by others.
- Catalog → publishes
entitlements.changedconsumed by Identity (claims), Config (defaults), Billing (SKUs), and Metering (dimension mapping). - Billing ↔ Metering → invoices derive from usage; subscription changes emit
entitlements.updated. - Config ↔ Identity → evaluation can use entitlements/roles; kill switches override downstream behavior safely.
- Audit → append-only records from every context; queries/export under retention and legal hold.
- Notifications/Webhooks → downstream of all; signed, idempotent, rate-limited.
Guardrails for safe evolution¶
- One command → one aggregate; cross-aggregate workflows via sagas with compensations.
- Additive-first contracts; breaking changes get new versions/topics/paths.
- Outbox/inbox everywhere; consumers are idempotent by design.
- Security/compliance defaults: classification and redaction, least-privilege keys, encrypted secrets, full audit with correlation.
Implementation read-off checklist¶
- Establish context repos and owners; seed code with the aggregate and repository templates.
- Stand up event topics per context with outbox publishing and inbox dedupe.
- Materialize read models needed for UI/ops (Directory, EntitlementsView, RealTimeCounters, Ledger, AuditTimeline).
- Wire policy engines (limits, proration, deprecation, precedence, dunning) behind application services.
- Enable admin and evaluation APIs with tenant-scoped auth; ship SDKs for config evaluation.
- Configure observability: structured logs with correlation, metrics for event flow and retries, traces across services.
- Enforce ADR-driven change management; every contract change accompanies an ADR and version bump.
- Integrate AI orchestration behind PR-only mutations with CI checks and reviewers.
Final note¶
This document is the stable scaffold for implementation. Teams can now open ADRs, create epics/features, and spin up services per bounded context with confidence that their models, contracts, and guardrails align. As the platform grows, iterate by extending—not breaking—the languages and contracts defined here.