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 consumesnumber.assigned.v1to 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— consumesValidateLeaseon every outbound message.routing-engine— reads inventory metadata (MNO/operator, prefix, block) to choose carriers.compliance-engine— producescompliance.tenant.suspended.v1, which triggers bulk recall of a tenant's leases.billing-service— bills per leased identifier; consumesnumber.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.
| Field | Type | Notes |
|---|---|---|
numberId | UUIDv4 | Internal identity, stable across state transitions |
value | NumberValue VO | Normalised canonical form (E.164 / digit string / uppercased alpha) |
type | enum NumberType | MSISDN · SHORT_CODE · ALPHA_ID |
subtype | enum NumberSubtype | STANDARD · VANITY · TOLL_FREE · PREMIUM_RATE · MNO_INTERNAL |
state | NumberState VO | AVAILABLE · RESERVED · HELD · LEASED · SUSPENDED · RECALLED · QUARANTINE (see §4) |
operatorId | UUIDv4 | null | MNO (Roshan/Etisalat-AF/MTN-AF/AWCC/Salaam) — NULL for short codes and alpha IDs |
leaseContractId | UUIDv4 | null | Originating MNO lease contract (MSISDN only) |
originatingBlockId | UUIDv4 | null | Import-batch reference (MSISDN / short code) |
assignedTenantId | UUIDv4 | null | Non-null iff state ∈ {RESERVED, HELD, LEASED, SUSPENDED} |
assignedLeaseId | UUIDv4 | null | Current active lease row (FK → Lease) |
quarantineUntil | Instant | null | Non-null iff state = QUARANTINE; cool-off expiry |
validFrom | Instant | MNO block effective-from date (future imports are inventory-loaded but excluded from ListAvailable) |
validUntil | Instant | null | MNO block expiry (for lease-bounded resources) |
version | int | Optimistic-lock counter — bumped on every state transition |
createdAt, updatedAt | Instant | — |
Invariants
value+typeis globally unique across all states exceptAVAILABLEfollowing a quarantine drop (single row, transitions in place).type = MSISDN:valuematches E.164 (^\+[1-9][0-9]{6,14}$); for Afghanistan,^\+93[0-9]{9}$.type = SHORT_CODE:valuematches^[0-9]{4,6}$; uniqueness is platform-wide (one 4-digit short code cannot co-exist per ATRA allocation).type = ALPHA_ID:valuematches^[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 = LEASED⇒assignedTenantId IS NOT NULLANDassignedLeaseId IS NOT NULL.state = QUARANTINE⇒quarantineUntil IS NOT NULL AND quarantineUntil > now().- An
ALPHA_IDwithstate = LEASEDrequires a corresponding verifiedSenderIdrow insender-id-registry-service— numbering-service does not verify KYC but rejectsAssignif the alpha-ID has not completed verification (read via sender-id-registry gRPCIsVerified). - 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.
| Field | Type | Notes |
|---|---|---|
leaseId | UUIDv4 | Identity |
numberId | UUIDv4 | FK → NumberResource |
tenantId | UUIDv4 | Owning tenant |
accountId | UUIDv4 | null | Optional sub-account scope |
effectiveFrom, effectiveUntil | Instant | Lease bounds |
term | LeaseTerm VO | P7D · P30D · P90D · P1Y · P3Y |
autoRenew | boolean | Triggers daily renewal job for leases within 7 d of expiry |
vanityFlag | boolean | Premium vanity short-code lease |
previousLeaseId | UUIDv4 | null | Populated on renewal chains |
createdBy | UUIDv4 | User who initiated the lease |
createdAt | Instant | — |
terminatedAt | Instant | null | Set when lease transitions to RECALLED |
terminationReason | enum | null | EXPIRED · REGULATOR_ORDER · ABUSE · NON_PAYMENT · TENANT_RELEASE · PLATFORM_RECALL |
Invariants
effectiveUntil > effectiveFrom.- At most one
Leaserow pernumberIdwithterminatedAt IS NULL. previousLeaseId(when set) must reference a row withterminationReason = EXPIRED(renewal) and the sametenantId.
Reservation
A short-lived hold on a NumberResource during a tenant's browse or build flow.
| Field | Type | Notes |
|---|---|---|
reservationId | UUIDv4 | Identity |
numberId | UUIDv4 | FK |
tenantId | UUIDv4 | Reserving tenant |
kind | enum | RESERVE (15 min) · HOLD (24 h) |
createdAt | Instant | — |
expiresAt | Instant | TTL deadline |
releasedAt | Instant | null | Populated on explicit release or TTL expiry |
releaseReason | enum | null | TTL_EXPIRED · TENANT_RELEASE · PROMOTED_TO_LEASE · PROMOTED_TO_HOLD |
Invariants
- Exactly one active reservation per
numberId(enforced by partial unique indexWHERE 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.
| Field | Type | Notes |
|---|---|---|
leaseContractId | UUIDv4 | Identity |
operatorId | UUIDv4 | FK → MobileOperator |
operatorMcc | string | MCC 412 (Afghanistan, ITU-T E.212) |
operatorMnc | string | MNC per operator (40 Roshan, 50 Etisalat-AF, 01/20 MTN-AF, 03 AWCC, 88 Salaam) |
prefixRange | PrefixRange VO | e.g. +9370… (Roshan), +9379… (Etisalat-AF) |
blockSize | int | Total MSISDNs in the block |
effectiveFrom, effectiveUntil | Instant | Contract validity |
autoRenew | boolean | MNO contract-level renewal signal (advisory; actual renegotiation is off-platform) |
signatureRef | string | Reference to the MNO's RSA signature on the import CSV |
status | enum | DRAFT · ACTIVE · EXPIRING · EXPIRED · SUSPENDED |
createdAt, updatedAt | Instant | — |
Invariants
prefixRangeis non-overlapping per(operatorMcc, operatorMnc)— ATRA Afghan Numbering Plan prohibits double allocation.effectiveUntil > effectiveFrom.- CSV imports for a contract require
status = ACTIVEand a valid signature (seeAPPLICATION_LOGICUC-ImportLeaseBatch).
TenantPool
Per-tenant inventory slice with quotas and policy overrides.
| Field | Type | Notes |
|---|---|---|
poolId | UUIDv4 | Identity |
tenantId | UUIDv4 | Owning tenant |
name | string | Human-readable |
maxLeasedMsisdn, maxLeasedShortCode, maxLeasedAlpha | int | Per-class quotas |
maxActiveReservations | int | Reservation burst guard |
allowedOperatorIds | UUID[] | MNO allowlist (tenant may purchase only from these) |
vanityEnabled | boolean | Premium tier gate |
bypassReservation | boolean | Direct AVAILABLE → LEASED (enterprise plan) |
createdAt, updatedAt | Instant | — |
Invariants
- Quotas are non-negative.
- A tenant may have exactly one
TenantPool; sub-pools are modelled viaaccountIdscope on leases, not separate pool rows.
QuarantineRecord
A timer entry for a number in cool-off after recall.
| Field | Type | Notes |
|---|---|---|
quarantineId | UUIDv4 | Identity |
numberId | UUIDv4 | FK |
previousTenantId | UUIDv4 | Last tenant before recall |
recallReason | enum | Inherited from terminating Lease.terminationReason |
quarantineFrom, quarantineUntil | Instant | Cool-off bounds |
overrideBy, overrideAt, overrideJustification | UUID, Instant, string | null | Set if an admin fast-tracks re-availability (NUM-US-006 §4) |
completedAt | Instant | null | Set 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.
| Field | Type | Notes |
|---|---|---|
eventId | UUIDv4 | Identity |
numberId | UUIDv4 | FK |
fromState, toState | NumberState | Transition |
leaseIdRef, reservationIdRef, quarantineIdRef | UUIDv4 | null | Reference to the driving aggregate |
actorUserId | UUIDv4 | null | Present for admin / tenant actions; null for system triggers |
actorService | string | null | Present for system triggers (e.g. compliance-engine, cron:reservation-cleanup) |
reasonCode | string | Enum-style reason (TENANT_LEASE, REGULATOR_ORDER, BILLING_FAILURE, etc.) |
prevHash, rowHash | bytea | SHA-256 chain (PG pgcrypto) |
occurredAt | Instant | — |
RegulatorExport
Monthly inventory snapshot for ATRA.
| Field | Type | Notes |
|---|---|---|
exportId | UUIDv4 | Identity |
periodYearMonth | string | YYYY-MM |
s3Ref | string | s3://ghasi-regulator-exports-{region}/numbering/{yyyy-mm}.csv.gz |
sha256Hex | string | Immutable content hash |
signatureRef | string | Platform RSA signature (ATRA-verifiable) |
submittedAt | Instant | null | ATRA submission timestamp |
status | enum | PENDING · GENERATED · SIGNED · SUBMITTED · ACCEPTED · REJECTED |
rowCount | int | Total inventory rows in snapshot |
3. Value Objects
| VO | Shape | Invariants |
|---|---|---|
NumberValue | discriminated on type | MSISDN → E.164; ShortCode → 4–6 digits; AlphaId → ^[A-Za-z0-9 \-]{1,11}$ |
NumberState | enum AVAILABLE · RESERVED · HELD · LEASED · SUSPENDED · RECALLED · QUARANTINE | See §4 |
NumberType | MSISDN · SHORT_CODE · ALPHA_ID | — |
NumberSubtype | STANDARD · VANITY · TOLL_FREE · PREMIUM_RATE · MNO_INTERNAL | — |
LeaseTerm | ISO-8601 duration: P7D, P30D, P90D, P1Y, P3Y | Whitelisted set only |
PrefixRange | { prefix: '+9370', fromSuffix: '0000000', toSuffix: '9999999' } | toSuffix >= fromSuffix; lengths match E.164 |
Msisdn | string (E.164) | Regex: ^\+[1-9][0-9]{6,14}$; for Afghanistan `^+93(7[0-9]{8} |
ShortCode | string (4–6 digits) | Regex: ^[0-9]{4,6}$ |
AlphaId | string (1–11) | GSM-7 default alphabet; no homoglyph substitution |
McC / Mnc | strings | ITU-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 anINVALID_TRANSITIONerror. LEASED → RECALLEDrequires a concurrentUPDATE leases SET terminated_at = now(), termination_reason = :reason WHERE lease_id = :active_lease_id— both writes in one transaction.RECALLED → QUARANTINEis atomic with inserting aQuarantineRecordrow.QUARANTINE → AVAILABLE(sweep cron) requiresquarantineUntil < 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 andSYNC_CONTRACT).
5. Domain Events (produced)
Detailed schemas in EVENT_SCHEMAS.md.
| Event | Trigger |
|---|---|
number.lease.imported.v1 | MNO CSV batch ingested into NumberResource inventory |
number.reserved.v1 | AVAILABLE → RESERVED or RESERVED → HELD |
number.released.v1 | Reservation / hold released (TTL or explicit) without assignment |
number.assigned.v1 | `RESERVED |
number.renewed.v1 | Lease auto-renewal extended effectiveUntil |
number.suspended.v1 | LEASED → SUSPENDED |
number.reinstated.v1 | SUSPENDED → LEASED |
number.recalled.v1 | `LEASED |
number.quarantine.started.v1 | RECALLED → QUARANTINE |
number.quarantine.completed.v1 | QUARANTINE → AVAILABLE |
number.conflict.detected.v1 | Cross-tenant claim or MNO range overlap |
number.pool.exhausted.v1 | Per-block capacity < threshold |
numbering.audit.v1 | Every lifecycle transition (mirrors the hash-chained audit row) |
numbering.regulator.export.generated.v1 | Monthly ATRA export file generated |
Consumed (detailed in EVENT_SCHEMAS.md §3):
| Subject | Producer | Purpose |
|---|---|---|
compliance.tenant.suspended.v1 | compliance-engine | Bulk-recall the tenant's leases (reason ABUSE) |
billing.account.delinquent.v1 | billing-service | Transition tenant leases to SUSPENDED |
senderid.revoked.v1 | sender-id-registry-service | Recall the corresponding alpha-ID lease |
tenant.deleted.v1 | auth-service | Release all reservations + recall all leases for the tenant |
mno.contract.updated.v1 | regulator-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
ValidateLeasefor 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 NULLonleases. - Cross-tenant claims impossible in normal flow. Partial unique index on
numbers(value, type) WHERE state IN ('RESERVED','HELD','LEASED','SUSPENDED'); race losers receiveCONFLICT. - 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-XYZwins the value nationally. - No orphan leases. A
Leaserow cannot exist without a correspondingNumberResource.state ∈ {LEASED, SUSPENDED}. - Multi-region consistency. Per ADR-0004 §14, control-plane writes use synchronous cross-region quorum on the
numbersandleasestables;reservationsare single-region with anti-affinity (reservations do not outlive a region failover cleanly — they are advisory TTL-bounded state).
7. Cross-Service Boundary Summary
| Concern | Owned by | Read/written by numbering-service |
|---|---|---|
| Alpha-ID KYC documents | sender-id-registry-service | Read via gRPC IsVerified(alphaId, tenantId) before Assign |
| Alpha-ID reputation | sender-id-registry-service + fraud-intel-service | Not read; advisory signal only |
| Tenant identity, plan tier | auth-service | Read via gRPC GetTenant(tenantId) |
| Billing per-lease fees | billing-service | Emits number.assigned.v1 / .released.v1 for billing to consume |
| Hot-path validation | numbering-service | Owned — ValidateLease gRPC, P95 ≤ 20 ms cache-hit |
| MNP / HLR lookups per message | number-intelligence-service | Not owned; separate service |
| Regulator-facing inventory reports | numbering-service → regulator-portal-service | Emits monthly export; portal handles ATRA submission workflow |
End of DOMAIN_MODEL.md