Skip to content

SaaS cross-repo published language and anti-corruption

Contract that governs how the five SaaS bounded-context repositories under C:\Git\ConnectSoft\ConnectSoft.Saas.*Template interoperate. This is the companion ADR to:

Products Catalog tenant partition

The Products Catalog template stores catalog rows with TenantId and applies NHibernate tenant filtering. Natural keys (product slug, feature key, etc.) are unique per tenant, not globally across all tenants in the database unless you deliberately use one canonical platform tenant for definitions. See ADR 0004 — Tenant-scoped catalog vs company SaaS DDD vocabulary in the Products Catalog template repo.

Non-negotiable rules

  1. No shared domain types across SaaS repos. Domain types live only in their owning repo (<Context>.DomainModel / <Context>.DomainModel.Impl).
  2. Consumers reference *.ServiceModel NuGets only. Referencing *.DomainModel, *.EntityModel, *.PersistenceModel.* across repos is forbidden and is enforced in <Context>.ArchitectureTests.
  3. Integration events are a first-class NuGet artifact, published from <Context>.MessagingModel. Common envelope fields (every event):
  4. tenantId (string, required)
  5. aggregateId (string, required)
  6. aggregateVersion (long)
  7. schemaVersion (int, starts at 1)
  8. correlationId (guid)
  9. causationId (guid, optional)
  10. occurredOn (datetime UTC)
  11. Additive versioning. v1 topics for the life of the envelope's backwards-compatible evolution. Breaking changes publish a new topic .v2 alongside .v1 (dual-publish + consumer opt-in migration).
  12. Transactional delivery via MassTransit built-in outbox. No custom outbox tables in our migrations; outbox schema belongs to MassTransit (configured via BaseTemplate's AddMassTransitOutbox extension in <Context>.ApplicationModel).
  13. Consumers are idempotent. Cross-repo reactions use MassTransit saga state machines (MassTransitStateMachine<TState>), not standalone IConsumer<T> handlers. Saga behaviors delegate to idempotent domain processor methods; duplicate message replays are no-ops. Inbox dedupe uses (sourceContext, eventId) via Redis or consumer persistence.
  14. Anti-corruption layer (ACL) for external providers. Any SaaS repo that consumes external provider semantics (payments, email/SMS, tax) isolates that integration under <Context>.FlowModel.MassTransit/Adapters/*. External shapes never leak into <Context>.DomainModel or cross-repo events.
  15. gRPC is the preferred high-throughput server-to-server surface. REST is the admin plane and external consumer surface. Both are stable NuGets (<Context>.ServiceModel.RestApi, <Context>.ServiceModel.Grpc).
  16. Orleans streams are intra-silo only. Cross-service integration always goes through MassTransit. Grains are tenant-scoped (grain key includes tenantId).

Canonical topic plan

Source repo Topic When
TenantsTemplate tenants.domain.v1.tenant-created Tenant aggregate provisioned
TenantsTemplate tenants.domain.v1.tenant-activated Status transition Pending -> Active
TenantsTemplate tenants.domain.v1.tenant-suspended Billing / compliance suspension
TenantsTemplate tenants.domain.v1.tenant-deleted Soft delete committed
TenantsTemplate tenants.domain.v1.residency-changed TenantRegionResidency changed
ProductsCatalogTemplate catalog.domain.v1.product-created New product registered
ProductsCatalogTemplate catalog.domain.v1.product-updated Product meta updated
ProductsCatalogTemplate catalog.domain.v1.product-retired Product lifecycle retired
ProductsCatalogTemplate catalog.domain.v1.edition-added New edition within product
ProductsCatalogTemplate catalog.domain.v1.edition-updated Edition metadata changed within product
ProductsCatalogTemplate catalog.domain.v1.feature-activated Feature enabled in an edition
ProductsCatalogTemplate catalog.domain.v1.feature-deactivated Feature disabled in an edition
ProductsCatalogTemplate catalog.entitlements.v1.entitlements-changed Fan-out to Entitlements + Billing
EntitlementsTemplate entitlements.v1.effective-entitlements-updated Per-tenant effective snapshot changed
EntitlementsTemplate entitlements.v1.feature-overridden Tenant feature override applied
EntitlementsTemplate entitlements.v1.assignment-changed Edition assignment flipped
BillingTemplate billing.subscriptions.v1.subscription-created New subscription
BillingTemplate billing.subscriptions.v1.subscription-upgraded Edition upgrade / downgrade committed
BillingTemplate billing.subscriptions.v1.subscription-canceled Cancellation or expiration
BillingTemplate billing.invoices.v1.invoice-issued Invoice projection finalized
BillingTemplate billing.payments.v1.payment-captured Payment provider callback reconciled
BillingTemplate billing.entitlements.v1.entitlements-sync-requested Request Entitlements re-materialize for a tenant
MeteringTemplate metering.usage.v1.usage-recorded Raw usage event
MeteringTemplate metering.counters.v1.counter-rolled Window boundary rolled
MeteringTemplate metering.quota.v1.quota-threshold-crossed Soft warning (e.g. 80%)
MeteringTemplate metering.quota.v1.quota-exceeded Hard ceiling hit
flowchart LR
  T[TenantsTemplate] -->|tenants.domain.v1.*| E[EntitlementsTemplate]
  T -->|tenants.domain.v1.*| B[BillingTemplate]
  T -->|tenants.domain.v1.*| M[MeteringTemplate]
  P[ProductsCatalogTemplate] -->|catalog.entitlements.v1.entitlements-changed| E
  P -->|catalog.domain.v1.*| B
  E -->|entitlements.v1.effective-entitlements-updated| B
  B -->|billing.entitlements.v1.entitlements-sync-requested| E
  B -->|billing.subscriptions.v1.*| M
  M -->|metering.quota.v1.*| B
  M -->|metering.quota.v1.*| E
Hold "Alt" / "Option" to enable pan & zoom

Sample seed IDs (E2E demo tenant)

Shared DefaultTenantId / seed tenant used across Billing, Metering, Tenants, Entitlements, and Products Catalog acceptance tests:

Constant Value Defined in
TenantId 7f4c2b9e-3d1a-4f8e-9c6b-5a0e183d42f1 SampleTenantSeed, SampleBillingSeed, SampleMeteringSeed, SampleEntitlementSeed
Catalog product (ConnectSoft Cloud Suite) 01a1b2c3-d4e5-6789-a012-345678900001 SampleCatalogSeed / SampleBillingSeed
Enterprise edition 02b2c3d4-e5f6-7890-b012-345678901004 SampleCatalogSeed / SampleBillingSeed
Sample subscription 21f1f2a3-b4c5-d6e7-f890-123456780011 SampleBillingSeed
Sample usage meter 20a0b1c2-d3e4-5678-f901-234567890abc SampleMeteringSeed

Per-repo seed documentation: docs/examples/sample-*-database.md in each ConnectSoft.Saas.*Template repository.

Edge consumer package matrix

Who may reference SaaS ServiceModel NuGets vs auth packages at the platform edge. Browsers and MFEs never reference Application, DomainModel, or PersistenceModel from SaaS repos.

Consumer ConnectSoft.Saas.*.ServiceModel* Auth / OIDC (Authorization Server, JWT middleware, BFF session) ConnectSoft.Saas.*.Application / *.DomainModel
API Gateway Yes — routing, OpenAPI aggregation, downstream typed clients Yes — token validation, scope checks; see API Gateway Template No
ConnectSoft.Blazor.Shell.Saas Yes — BFF calls to bounded contexts Yes — OIDC login, cookie/BFF session; see Authorization Server Template No
Blazor MFE (admin / self-service) Yes — ServiceModel HTTP/gRPC clients only Yes — via shell/host OIDC; MFE does not embed identity domain types No
Platform admin tools Yes — same as MFE edge rule Yes — operator auth separate from SaaS domain No
Peer SaaS microservice Yes — see matrix below Service auth as configured (mTLS, internal JWT) No

Auth packages at the edge are identity-plane artifacts (Authorization Server, ASP.NET Core authentication middleware, OIDC client libraries)—not substitutes for ConnectSoft.Saas.<Context>.ServiceModel. Business operations (catalog, billing, entitlements, …) always flow through ServiceModel contracts.

ServiceModel matrix (who consumes whose REST/gRPC NuGet)

Consumer TenantsSM CatalogSM EntitlementsSM BillingSM MeteringSM
TenantsTemplate
ProductsCatalogTemplate yes
EntitlementsTemplate yes yes yes
BillingTemplate yes yes yes yes
MeteringTemplate yes yes yes
Gateway / Shell / MFEs yes yes yes yes yes
Platform (admin) yes yes yes yes yes

"yes" means the consumer takes a dependency on that repo's published NuGets:

  • <Source>.ServiceModel (DTOs + typed clients)
  • <Source>.ServiceModel.RestApi (controllers surface declarations when hosted by the consumer — e.g. Gateway OpenAPI aggregation)
  • <Source>.ServiceModel.Grpc (code-first gRPC server adapter classes implementing the C# [ServiceContract] interfaces from <Source>.ServiceModel; ServiceModel.Grpc — no .proto)
  • <Source>.MessagingModel (integration event DTOs)

Idempotency + inbox

  • Inbox key: sha256(sourceContext || eventId || schemaVersion).
  • Storage: Redis set (TTL 7 days) by default. Repos that persist domain state in SQL may move the inbox into a dedicated _inbox table (no outbox — only inbox — and it lives in the repo's own migration, since it is internal state).
  • Duplicate handling: consumers return "ack" but skip side effects; metric masstransit_inbox_duplicates_total records the event for observability.

Breaking-change procedure (topic bump)

  1. Publish new topic .v2 alongside .v1 (dual publish).
  2. Update consumers at their cadence.
  3. Announce deprecation of .v1 with a hard date.
  4. Remove .v1 publisher (and topic) once consumer telemetry shows zero traffic for the deprecation window.

Enforcement

  • <Context>.ArchitectureTests fails when a cross-repo reference violates rule #2.
  • azure-pipelines-messaging-model.yml runs a json-schema smoke test that every event type in <Context>.MessagingModel conforms to the common envelope.
  • azure-pipelines-service-model.yml re-runs architecture tests (gRPC ArchitectureTests prove zero .proto files in *.ServiceModel.Grpc and enforce [ServiceContract]/[OperationContract] on all *.ServiceModel interfaces) + OpenAPI schema validation (REST) to guarantee additive-only diffs within a v1 major.

Changelog

  • 2026-05-27 — Added edge consumer package matrix (Gateway / Shell / MFE vs ServiceModel vs auth); anchor IDs for cross-links.
  • 2026-05-26 — Added sample seed IDs table; documented saga-based cross-repo consumption; corrected template repo paths.
  • 2026-05-19 — Documented tenant-partitioned catalog keys for Products Catalog (link to ADR 0004).
  • 2026-05-18 — Added catalog.domain.v1.edition-updated to the canonical topic plan (Products Catalog).
  • 2026-04-28 — Initial version aligned with the five SaaS template repos.