Skip to main content

numbering-service — Domain Model

Version: 1.0 Status: Draft Owner: Commerce Engineering + Platform Engineering Last Updated: 2026-04-21 Companion: SERVICE_OVERVIEW · APPLICATION_LOGIC · DATA_MODEL · EVENT_SCHEMAS · API_CONTRACTS · SECURITY_MODEL


1. Bounded Context

National Numbering-Plan Authority. The numbering-service is the canonical ledger for every sendable identifier in the Ghasi-SMS-Gateway national backbone — MSISDNs (ITU-T E.164 long codes), short codes (4-6 digits), and alphanumeric sender IDs (3GPP TS 23.038, 1–11 GSM-7 characters). It answers one core question on the hot path: "Is this identifier currently leased to this tenant in a valid state?"

The service sits adjacent to (and must not duplicate the responsibilities of):

  • sender-id-registry-service — owns KYC, verification, reputation of alpha-ID registrants. Numbering-service owns the inventory slot + lease record; sender-id-registry consumes number.assigned.v1 to know when an alpha-ID value becomes unavailable at the inventory layer.
  • number-intelligence-service — owns real-time HLR / MNP / EIR intelligence per MSISDN at dispatch time.
  • sms-orchestrator — consumes ValidateLease on every outbound message.
  • routing-engine — reads inventory metadata (MNO/operator, prefix, block) to choose carriers.
  • compliance-engine — produces compliance.tenant.suspended.v1, which triggers bulk recall of a tenant's leases.
  • billing-service — bills per leased identifier; consumes number.assigned.v1 / number.released.v1.
  • fraud-intel-service — receives anomaly signals (e.g., sudden burst of short-code reservations from one tenant) from numbering.
  • regulator-portal-service — consumes monthly inventory exports for ATRA (Afghanistan Telecom Regulatory Authority).

The context boundary is drawn such that:

  • Inside the boundary: MSISDN / short-code / alpha-ID inventory rows; lease contracts with MNOs (Roshan MCC/MNC 412/40, Etisalat-AF 412/50, MTN-AF 412/01 & 412/20, AWCC 412/03, Salaam 412/88); tenant pools and quotas; reservations and holds with TTL; quarantine records; lifecycle state machine; regulator-export records; audit log.
  • Outside the boundary: KYC artefacts (sender-id-registry), MNP/HLR lookups (number-intelligence), message ingestion (sms-orchestrator), carrier routing (routing-engine), billing invoices (billing-service), fraud reputation (fraud-intel).

Per ADR-0004 §3, numbering-service is one of the twelve national-backbone bounded contexts. Per ADR-0004 §14, numbering inventory is multi-master control-plane data across the kbl and mzr regions with strict per-row CAS (compare-and-swap) on state transitions — no double-assignment is possible.


2. Aggregates

NumberResource

The root aggregate. One row per registered identifier (MSISDN, short code, or alpha ID) on the national platform. Lifecycle is deterministic and fully audited.

FieldTypeNotes
numberIdUUIDv4Internal identity, stable across state transitions
valueNumberValue VONormalised canonical form (E.164 / digit string / uppercased alpha)
typeenum NumberTypeMSISDN · SHORT_CODE · ALPHA_ID
subtypeenum NumberSubtypeSTANDARD · VANITY · TOLL_FREE · PREMIUM_RATE · MNO_INTERNAL
stateNumberState VOAVAILABLE · RESERVED · HELD · LEASED · SUSPENDED · RECALLED · QUARANTINE (see §4)
operatorIdUUIDv4 | nullMNO (Roshan/Etisalat-AF/MTN-AF/AWCC/Salaam) — NULL for short codes and alpha IDs
leaseContractIdUUIDv4 | nullOriginating MNO lease contract (MSISDN only)
originatingBlockIdUUIDv4 | nullImport-batch reference (MSISDN / short code)
assignedTenantIdUUIDv4 | nullNon-null iff state ∈ {RESERVED, HELD, LEASED, SUSPENDED}
assignedLeaseIdUUIDv4 | nullCurrent active lease row (FK → Lease)
quarantineUntilInstant | nullNon-null iff state = QUARANTINE; cool-off expiry
validFromInstantMNO block effective-from date (future imports are inventory-loaded but excluded from ListAvailable)
validUntilInstant | nullMNO block expiry (for lease-bounded resources)
versionintOptimistic-lock counter — bumped on every state transition
createdAt, updatedAtInstant

Invariants

  • value + type is globally unique across all states except AVAILABLE following a quarantine drop (single row, transitions in place).
  • type = MSISDN: value matches E.164 (^\+[1-9][0-9]{6,14}$); for Afghanistan, ^\+93[0-9]{9}$.
  • type = SHORT_CODE: value matches ^[0-9]{4,6}$; uniqueness is platform-wide (one 4-digit short code cannot co-exist per ATRA allocation).
  • type = ALPHA_ID: value matches ^[A-Za-z0-9 \-]{1,11}$ per 3GPP TS 23.038 default alphabet; uppercased for uniqueness but display preserves original case.
  • Transitions between states are strictly governed by §4; illegal transitions are rejected with INVALID_TRANSITION.
  • state = LEASEDassignedTenantId IS NOT NULL AND assignedLeaseId IS NOT NULL.
  • state = QUARANTINEquarantineUntil IS NOT NULL AND quarantineUntil > now().
  • An ALPHA_ID with state = LEASED requires a corresponding verified SenderId row in sender-id-registry-service — numbering-service does not verify KYC but rejects Assign if the alpha-ID has not completed verification (read via sender-id-registry gRPC IsVerified).
  • Cross-tenant uniqueness for active states: only one row may hold state ∈ {RESERVED, HELD, LEASED, SUSPENDED} for a given (value, type) tuple — enforced by partial unique index.

Lease

A concrete assignment of a NumberResource to a tenant for a bounded term. Append-only per lease lifecycle — a renewal inserts a new row referencing the prior leaseId.

FieldTypeNotes
leaseIdUUIDv4Identity
numberIdUUIDv4FK → NumberResource
tenantIdUUIDv4Owning tenant
accountIdUUIDv4 | nullOptional sub-account scope
effectiveFrom, effectiveUntilInstantLease bounds
termLeaseTerm VOP7D · P30D · P90D · P1Y · P3Y
autoRenewbooleanTriggers daily renewal job for leases within 7 d of expiry
vanityFlagbooleanPremium vanity short-code lease
previousLeaseIdUUIDv4 | nullPopulated on renewal chains
createdByUUIDv4User who initiated the lease
createdAtInstant
terminatedAtInstant | nullSet when lease transitions to RECALLED
terminationReasonenum | nullEXPIRED · REGULATOR_ORDER · ABUSE · NON_PAYMENT · TENANT_RELEASE · PLATFORM_RECALL

Invariants

  • effectiveUntil > effectiveFrom.
  • At most one Lease row per numberId with terminatedAt IS NULL.
  • previousLeaseId (when set) must reference a row with terminationReason = EXPIRED (renewal) and the same tenantId.

Reservation

A short-lived hold on a NumberResource during a tenant's browse or build flow.

FieldTypeNotes
reservationIdUUIDv4Identity
numberIdUUIDv4FK
tenantIdUUIDv4Reserving tenant
kindenumRESERVE (15 min) · HOLD (24 h)
createdAtInstant
expiresAtInstantTTL deadline
releasedAtInstant | nullPopulated on explicit release or TTL expiry
releaseReasonenum | nullTTL_EXPIRED · TENANT_RELEASE · PROMOTED_TO_LEASE · PROMOTED_TO_HOLD

Invariants

  • Exactly one active reservation per numberId (enforced by partial unique index WHERE released_at IS NULL).
  • kind = RESERVE: expiresAt = createdAt + 15 min.
  • kind = HOLD: expiresAt = createdAt + 24 h.

LeaseContract

Per-MNO contract record governing a batch of MSISDNs leased to the platform from a carrier. Numbering-service redistributes leases from this wholesale pool to tenants.

FieldTypeNotes
leaseContractIdUUIDv4Identity
operatorIdUUIDv4FK → MobileOperator
operatorMccstringMCC 412 (Afghanistan, ITU-T E.212)
operatorMncstringMNC per operator (40 Roshan, 50 Etisalat-AF, 01/20 MTN-AF, 03 AWCC, 88 Salaam)
prefixRangePrefixRange VOe.g. +9370… (Roshan), +9379… (Etisalat-AF)
blockSizeintTotal MSISDNs in the block
effectiveFrom, effectiveUntilInstantContract validity
autoRenewbooleanMNO contract-level renewal signal (advisory; actual renegotiation is off-platform)
signatureRefstringReference to the MNO's RSA signature on the import CSV
statusenumDRAFT · ACTIVE · EXPIRING · EXPIRED · SUSPENDED
createdAt, updatedAtInstant

Invariants

  • prefixRange is non-overlapping per (operatorMcc, operatorMnc) — ATRA Afghan Numbering Plan prohibits double allocation.
  • effectiveUntil > effectiveFrom.
  • CSV imports for a contract require status = ACTIVE and a valid signature (see APPLICATION_LOGIC UC-ImportLeaseBatch).

TenantPool

Per-tenant inventory slice with quotas and policy overrides.

FieldTypeNotes
poolIdUUIDv4Identity
tenantIdUUIDv4Owning tenant
namestringHuman-readable
maxLeasedMsisdn, maxLeasedShortCode, maxLeasedAlphaintPer-class quotas
maxActiveReservationsintReservation burst guard
allowedOperatorIdsUUID[]MNO allowlist (tenant may purchase only from these)
vanityEnabledbooleanPremium tier gate
bypassReservationbooleanDirect AVAILABLE → LEASED (enterprise plan)
createdAt, updatedAtInstant

Invariants

  • Quotas are non-negative.
  • A tenant may have exactly one TenantPool; sub-pools are modelled via accountId scope on leases, not separate pool rows.

QuarantineRecord

A timer entry for a number in cool-off after recall.

FieldTypeNotes
quarantineIdUUIDv4Identity
numberIdUUIDv4FK
previousTenantIdUUIDv4Last tenant before recall
recallReasonenumInherited from terminating Lease.terminationReason
quarantineFrom, quarantineUntilInstantCool-off bounds
overrideBy, overrideAt, overrideJustificationUUID, Instant, string | nullSet if an admin fast-tracks re-availability (NUM-US-006 §4)
completedAtInstant | nullSet when cool-off completes and resource returns to AVAILABLE

Invariants

  • Default cool-off durations (per NumberType): MSISDN = 90 d; Short Code = 30 d; Alpha ID = 0 d (alpha is per-tenant scope only — no cross-tenant recycling risk). Vanity short codes = 365 d.
  • quarantineUntil >= quarantineFrom.
  • override* fields require all three set together; null set together otherwise.

NumberLifecycleEvent (append-only audit)

Every state transition writes one row, hash-chained for tamper-evidence.

FieldTypeNotes
eventIdUUIDv4Identity
numberIdUUIDv4FK
fromState, toStateNumberStateTransition
leaseIdRef, reservationIdRef, quarantineIdRefUUIDv4 | nullReference to the driving aggregate
actorUserIdUUIDv4 | nullPresent for admin / tenant actions; null for system triggers
actorServicestring | nullPresent for system triggers (e.g. compliance-engine, cron:reservation-cleanup)
reasonCodestringEnum-style reason (TENANT_LEASE, REGULATOR_ORDER, BILLING_FAILURE, etc.)
prevHash, rowHashbyteaSHA-256 chain (PG pgcrypto)
occurredAtInstant

RegulatorExport

Monthly inventory snapshot for ATRA.

FieldTypeNotes
exportIdUUIDv4Identity
periodYearMonthstringYYYY-MM
s3Refstrings3://ghasi-regulator-exports-{region}/numbering/{yyyy-mm}.csv.gz
sha256HexstringImmutable content hash
signatureRefstringPlatform RSA signature (ATRA-verifiable)
submittedAtInstant | nullATRA submission timestamp
statusenumPENDING · GENERATED · SIGNED · SUBMITTED · ACCEPTED · REJECTED
rowCountintTotal inventory rows in snapshot

3. Value Objects

VOShapeInvariants
NumberValuediscriminated on typeMSISDN → E.164; ShortCode → 4–6 digits; AlphaId → ^[A-Za-z0-9 \-]{1,11}$
NumberStateenum AVAILABLE · RESERVED · HELD · LEASED · SUSPENDED · RECALLED · QUARANTINESee §4
NumberTypeMSISDN · SHORT_CODE · ALPHA_ID
NumberSubtypeSTANDARD · VANITY · TOLL_FREE · PREMIUM_RATE · MNO_INTERNAL
LeaseTermISO-8601 duration: P7D, P30D, P90D, P1Y, P3YWhitelisted set only
PrefixRange{ prefix: '+9370', fromSuffix: '0000000', toSuffix: '9999999' }toSuffix >= fromSuffix; lengths match E.164
Msisdnstring (E.164)Regex: ^\+[1-9][0-9]{6,14}$; for Afghanistan `^+93(7[0-9]{8}
ShortCodestring (4–6 digits)Regex: ^[0-9]{4,6}$
AlphaIdstring (1–11)GSM-7 default alphabet; no homoglyph substitution
McC / MncstringsITU-T E.212; MCC = 412 (AF)

4. Lifecycle State Machine

Invariants on transitions:

  • Every transition is CAS-protected: UPDATE numbers SET state = :new, version = version + 1 WHERE number_id = :id AND version = :expected AND state = :expected_state. Second writer gets zero rows affected and an INVALID_TRANSITION error.
  • LEASED → RECALLED requires a concurrent UPDATE leases SET terminated_at = now(), termination_reason = :reason WHERE lease_id = :active_lease_id — both writes in one transaction.
  • RECALLED → QUARANTINE is atomic with inserting a QuarantineRecord row.
  • QUARANTINE → AVAILABLE (sweep cron) requires quarantineUntil < now(); override path requires an admin user + justification captured in the event.
  • Events (number.*.v1) are written to the outbox in the same transaction as the state row mutation (§5 and SYNC_CONTRACT).

5. Domain Events (produced)

Detailed schemas in EVENT_SCHEMAS.md.

EventTrigger
number.lease.imported.v1MNO CSV batch ingested into NumberResource inventory
number.reserved.v1AVAILABLE → RESERVED or RESERVED → HELD
number.released.v1Reservation / hold released (TTL or explicit) without assignment
number.assigned.v1`RESERVED
number.renewed.v1Lease auto-renewal extended effectiveUntil
number.suspended.v1LEASED → SUSPENDED
number.reinstated.v1SUSPENDED → LEASED
number.recalled.v1`LEASED
number.quarantine.started.v1RECALLED → QUARANTINE
number.quarantine.completed.v1QUARANTINE → AVAILABLE
number.conflict.detected.v1Cross-tenant claim or MNO range overlap
number.pool.exhausted.v1Per-block capacity < threshold
numbering.audit.v1Every lifecycle transition (mirrors the hash-chained audit row)
numbering.regulator.export.generated.v1Monthly ATRA export file generated

Consumed (detailed in EVENT_SCHEMAS.md §3):

SubjectProducerPurpose
compliance.tenant.suspended.v1compliance-engineBulk-recall the tenant's leases (reason ABUSE)
billing.account.delinquent.v1billing-serviceTransition tenant leases to SUSPENDED
senderid.revoked.v1sender-id-registry-serviceRecall the corresponding alpha-ID lease
tenant.deleted.v1auth-serviceRelease all reservations + recall all leases for the tenant
mno.contract.updated.v1regulator-portal-service (ATRA MoU changes)Update LeaseContract.effectiveUntil

6. Global Invariants

  • Fail-closed on writes. If PostgreSQL is unavailable, no Reserve / Assign / Release / Recall call succeeds. Readers can serve Redis-cached ValidateLease for up to 60 s.
  • Strict CAS on every state transition. Prevents double-assignment under concurrent Reserve / Assign in multi-region deployments per ADR-0004 §14.
  • One active lease per number. Enforced by partial unique index (number_id) WHERE terminated_at IS NULL on leases.
  • Cross-tenant claims impossible in normal flow. Partial unique index on numbers(value, type) WHERE state IN ('RESERVED','HELD','LEASED','SUSPENDED'); race losers receive CONFLICT.
  • Quarantine cool-off is not bypassable by the same tenant. A tenant recalling their own number cannot re-lease it during quarantine; override requires a platform admin action logged with justification.
  • Hash-chained audit. NumberLifecycleEvent.rowHash = sha256(prevHash || row_body) — any tampering invalidates the chain (verified by daily integrity cron).
  • Alpha-ID platform uniqueness. Unlike MSISDNs (per-MNO prefix scope), alpha-IDs are platform-wide unique — the first tenant to lease BANK-XYZ wins the value nationally.
  • No orphan leases. A Lease row cannot exist without a corresponding NumberResource.state ∈ {LEASED, SUSPENDED}.
  • Multi-region consistency. Per ADR-0004 §14, control-plane writes use synchronous cross-region quorum on the numbers and leases tables; reservations are single-region with anti-affinity (reservations do not outlive a region failover cleanly — they are advisory TTL-bounded state).

7. Cross-Service Boundary Summary

ConcernOwned byRead/written by numbering-service
Alpha-ID KYC documentssender-id-registry-serviceRead via gRPC IsVerified(alphaId, tenantId) before Assign
Alpha-ID reputationsender-id-registry-service + fraud-intel-serviceNot read; advisory signal only
Tenant identity, plan tierauth-serviceRead via gRPC GetTenant(tenantId)
Billing per-lease feesbilling-serviceEmits number.assigned.v1 / .released.v1 for billing to consume
Hot-path validationnumbering-serviceOwnedValidateLease gRPC, P95 ≤ 20 ms cache-hit
MNP / HLR lookups per messagenumber-intelligence-serviceNot owned; separate service
Regulator-facing inventory reportsnumbering-serviceregulator-portal-serviceEmits monthly export; portal handles ATRA submission workflow

End of DOMAIN_MODEL.md