Skip to content

ADR-012: Explicit Public Service Contracts for Synchronous Module Calls ​

Status ​

Accepted

Context ​

Admin workflows create and query identity accounts, organizations, and members, and orchestrate subscription-owned plan, subscription, invoice and payment operations. Using the event bus for commands that must complete during an HTTP request would introduce eventual consistency, correlation, and retry handling. Passing entire service modules into admin would expose unrelated operations and make module boundaries difficult to enforce.

The earlier admin Tenant projection also duplicated organization lifecycle state even though the organization service is its authoritative owner.

Decision ​

  • Commands and queries required to complete the current request use direct, in-process calls.
  • Each provider exposes manually curated contracts in its public package.
  • Public packages contain operation interfaces, DTOs, pagination types, and stable errors only. They do not expose repositories, domain entities, handlers, adapters, or module lifecycle types.
  • Consumers accept curated public contracts scoped to their workflows. Admin uses one AdminOrganizationService for organization and membership operations and one AdminIdentityService for account provisioning and lookups. No service receives another service's complete module.
  • The API composition root obtains provider services and capabilities and wires them into consumers.
  • The owning service publishes canonical events after successful state changes. Events are used for notifications, audit, analytics, and projections, not as synchronous command transport.
  • Organization remains the sole owner of organization lifecycle and membership data. Admin does not maintain a duplicate Tenant entity.

Dependency Direction ​

text
admin -> identity/public
admin -> organization/public
admin -> subscription/public
organization -> doctor/public + subscription/public (shared response contracts)
doctor -> organization/public (affiliation and request contracts)

identity/module -> identity internals + identity/public
organization/module -> organization internals + organization/public

prescription -> patient/public (patient data and future authorized lookups)
patient has no dependency on prescription

api composition root -> all module constructors

Identity and organization never import admin, preventing circular dependencies. When a module is extracted, an HTTP or gRPC client can replace its in-process facade without changing the consuming use cases.

Organization and Identity expose their Admin service contracts through Module.Administration(). Admin's internal ports alias those public contracts to avoid duplicate interface definitions. AdminOrganizationService combines creation, individual and paginated reads, editing, lifecycle changes, member creation, and member lookup. AdminIdentityService combines account creation and individual/batch lookups. Identity verification remains the separate types.IIdentityService contract; other services and authorization/audit adapters can continue to consume Identity's read-only AccountReader capability.

Subscription exposes individual billing-operation capabilities. Admin does not receive subscription repositories or the complete subscription module. Organization billing responses reuse subscription-owned DTOs. The current billing use cases delegate to subscription TODO operations returning 501, without authentication, persistence, or event publication (ADR-014). Actual state changes and after-commit events remain future implementation.

Patient exposes patient/public.PatientReader through patient/module.Patients(), using curated DTOs and Authorization/PatientID request context. Its facade currently returns ErrNotImplemented; authorization and lookups are not implemented. Prescription history remains prescription-owned; Patient does not call back into Prescription. Actual cross-service calls are deferred while these HTTP operations return 501 (ADR-015).

Medicine administration follows the same capability-scoped boundary: admin -> medicine/public. The composition root selects each catalog operation individually; Admin owns permission orchestration, Medicine owns catalog behavior. These operations return ErrNotImplemented until business implementation (ADR-016).

Consequences ​

Positive ​

  • Cross-service dependencies are explicit and scoped to consumer workflows.
  • Synchronous operations return authoritative results immediately.
  • Internal domain and persistence models remain private.
  • Module extraction changes adapters rather than application logic.
  • Canonical events have a single publisher and avoid duplicate delivery.

Negative ​

  • Public DTOs require mapping at each service boundary.
  • Providers must maintain stable public contracts.
  • Multi-service workflows may need idempotency or compensation when a later direct operation fails.

Superseded Details ​

This decision supersedes the Tenant ownership described for admin in ADR-008 and the /api/v1/admin/tenants/* examples in ADR-009. Admin-facing organization routes use /api/v1/admin/organizations/* and delegate to organization-owned capabilities.

Documentation follows the source. Planned contracts are marked explicitly.