Analytics Service — Domain Model
Status: populated Owner: Platform Engineering Last updated: 2026-04-18 Companion: SERVICE_OVERVIEW · DATA_MODEL
1. Aggregates
This service is a read-side / projection service — it has no business aggregates with their own invariants. All writes are idempotent projections from consumed events. However, the following projection roots are defined for clarity:
HourlyMetricBucket (projection root)
Represents a one-hour window of aggregated platform activity.
| Field | Type | Notes |
|---|---|---|
bucketId | composite | (hourBucket, operatorId | accountId | 'platform') |
hourBucket | Instant | Truncated to the hour: date_trunc('hour', event_at) |
scope | MetricScope enum | OPERATOR, ACCOUNT, PLATFORM |
scopeId | string | operatorId, accountId, or 'platform' |
totalMessages | bigint | Submitted messages in window |
deliveredMessages | bigint | DLR confirmed DELIVERED |
failedMessages | bigint | DLR confirmed FAILED or UNDELIVERED |
pendingMessages | bigint | No DLR received within window |
totalCost | decimal(18,6) | Sum of wholesale cost from billing events |
avgLatencyMs | integer | Average DLR latency in window |
p95LatencyMs | integer | P95 DLR latency (approximate) |
peakTps | integer | Highest 1-second rate observed in window |
errorRate | decimal(6,4) | failedMessages / totalMessages |
DailyMetricBucket (projection root)
Roll-up of HourlyMetricBucket rows into a 24-hour window. Same fields, different granularity.
2. Value Objects
| VO | Invariant |
|---|---|
HourBucket | TIMESTAMPTZ with minutes/seconds truncated to 00:00 |
DayBucket | TIMESTAMPTZ with time truncated to 00:00:00 |
DeliveryRate | 0.0–1.0 (ratio); computed as deliveredMessages / totalMessages |
LatencyMs | Non-negative integer |
3. Projection Rules
billing.events → metrics
| Billing event field | Projection action |
|---|---|
accountId | Increment account scope bucket |
operatorId | Increment operator scope bucket |
cost | Add to totalCost |
segmentCount | Multiply with cost for segment-level totals |
status = 'BILLED' | Increment totalMessages |
sms.dlr.inbound → delivery metrics
| DLR field | Projection action |
|---|---|
deliveryStatus = 'DELIVERED' | Increment deliveredMessages |
deliveryStatus = 'FAILED' | Increment failedMessages |
latencyMs | Accumulate for avg/P95 computation |
operatorId | Update operator scope bucket |
4. Idempotency Model
Each bucket UPSERT is keyed on (hour_bucket, scope, scope_id). Counters use counter + EXCLUDED.counter pattern in PostgreSQL ON CONFLICT DO UPDATE. Replayed events produce identical SQL and do not double-count.
For events that carry a unique eventId: a secondary anlyt.processed_events deduplication table ensures each eventId is processed exactly once within the 48-hour dedup window.
5. Domain Events
This service publishes no events. It is a pure consumer/projection.