What each service is for — its mission, the world it sees, and (just as important) its explicit non-responsibilities. This sits between the architecture overview and the sequence flows: the overview shows the boxes, the flows show them moving, this page explains why each box exists.
Every service follows the same physique: owns its data outright (database-per-service), speaks HTTP only through the gateway, and reacts to the world through events.
Mission: be the only thing the internet can reach — authenticate once, authorize per route, proxy to the owning service, and compose cross-service reads without ever owning data.
flowchart LR
T([Tenant]) --> GW
M([Manager]) --> GW
TE([Technician]) --> GW
subgraph GW [API Gateway]
direction TB
AUTH[auth module<br/>login · JwtAuthGuard · RolesGuard]
PASS[pass-through controllers<br/>work-orders · properties · activity]
SUM[PropertySummaryService<br/>parallel compose + degrade]
DC[DownstreamClient<br/>3s timeout · 502/504 translate<br/>x-request-id + x-user-id]
DOCS[Scalar /api/docs<br/>+ /api/docs-json]
end
GW -->|HTTP| WO[Work Orders]
GW -->|HTTP| PR[Properties]
GW -->|HTTP| AU[Audit]
Owns: nothing durable — the gateway is stateless by design; restarting it loses nothing.
Responsibilities: JWT verification and role policy (ADR-0008) · identity/correlation propagation · timeout + fail-fast on every downstream call · graceful degradation in composition (flow 6) · publishing the API reference.
Explicitly NOT its job: payload validation (lives with the service that owns the data — a rule change must not be a two-deploy event) · business rules · talking to brokers or databases.
Mission: own the maintenance request from birth to terminal state — the state machine, the AI triage, and the only place domain events are born.
flowchart LR
GW[Gateway] -->|HTTP| SVC
Q[queue work-orders.triage] -->|own created events| TR
subgraph SVC [Work Orders Service]
direction TB
CT[controller + DTO validation]
SM[WorkOrdersService<br/>state machine guard]
TR[triage module<br/>consumer · classifier seam]
OB[outbox stage<br/>same-transaction write]
RL[OutboxRelay<br/>SKIP LOCKED poller]
end
TR -.->|classify| AI[Anthropic API]
RL -->|publish| MQ[(RabbitMQ)]
RL -->|produce, key = workOrderId| KF[(Kafka)]
SVC --- DB[(work_orders<br/>outbox_events)]
Owns: the work_orders table, the outbox_events staging table, the lifecycle rules (flow 8) and the triage vocabulary.
Responsibilities: every state transition, guarded · staging state + event atomically (ADR-0007) · relaying to both brokers · reacting to its own created event with best-effort LLM classification (ADR-0006).
Explicitly NOT its job: knowing who gets notified · serving the activity feed · property data (it stores propertyId as an opaque reference — never a foreign key into another service's database).
Mission: be the source of truth for buildings and their managers — the stable entity the volatile work orders point at.
flowchart LR
GW[Gateway] -->|HTTP<br/>create · list · get| SVC
subgraph SVC [Properties Service]
direction TB
CT[controller + DTO validation]
S[PropertiesService]
end
SVC --- DB[(properties)]
Owns: the properties table.
Responsibilities: CRUD with validation, nothing more — and that is the point: a service is allowed to be small when its domain is small.
Explicitly NOT its job: knowing that work orders exist. The reference goes one way (work order → propertyId); the gateway composes the two when a view needs both (flow 6). It emits no events yet — a second event producer would need its own outbox.
Mission: turn domain events into messages humans receive — and absorb every delivery failure so the rest of the system never waits for an email.
flowchart LR
MQ[(RabbitMQ)] -->|work-order.created| Q1[notifications.work-order-created]
MQ -->|work-order.completed| Q2[notifications.work-order-completed]
Q1 --> SVC
Q2 --> SVC
subgraph SVC [Notifications Service]
direction TB
CO[WorkOrderEventsConsumer]
DED[processed-events store<br/>dedup by eventId]
RET[EventRetryHandler<br/>TTL retry x3 → DLQ]
SND[NotificationSender seam]
end
SVC -.->|failed attempts| RQ[.retry queues TTL 5s]
SVC -.->|exhausted| DLQ[.dlq — parked for humans]
SND --> OUT([manager / tenant])
Owns: nothing durable — no database, no HTTP API beyond health/metrics. Its only state is the in-memory dedup store (the documented reason it runs as a single replica).
Responsibilities: consuming with at-least-once discipline (dedup, mark-after-send) · the retry/DLQ machinery (flow 5) · keeping the sender behind a seam so channels (email/SMS/push) are an implementation detail.
Explicitly NOT its job: being a source of truth for anything. If it vanished, no data would be lost — only messages delayed. That property is what "pure reactor" means.
Mission: answer "what happened, when, and who did it" — by projecting the replayable Kafka log into a queryable feed, forever.
flowchart LR
KF[(Kafka<br/>propflow.work-orders)] -->|consumer group audit-service<br/>from beginning| ING
GW[Gateway] -->|GET /activity| FEED
subgraph SVC [Audit Service]
direction TB
ING[AuditStreamConsumer<br/>ON CONFLICT DO NOTHING]
FEED[ActivityFeedService<br/>keyset pagination]
end
SVC --- DB[(audit_events<br/>disposable projection)]
Owns: the audit_events projection — deliberately disposable: the source of truth is the log, and a fresh consumer group at offset 0 rebuilds the table from history (ADR-0002).
Responsibilities: idempotent ingestion of every domain event (including the AI's triage decisions, with actorId answering who) · serving the feed with cursor pagination stable under constant appends (flow 7).
Explicitly NOT its job: writing domain state — it never produces an event, never calls another service, and its table can be truncated without losing anything the log still holds.
Three patterns repeat across every section above, and they are the architecture:
- Ownership is absolute — each arrow into a database comes from exactly one service; every cross-service reference is an opaque id.
- Sync for questions, async for consequences — the gateway's arrows carry queries; everything that happens because of a change travels as an event.
- A service's value shows in what it refuses to do — the gateway refuses validation, properties refuses to know work orders, notifications refuses to own truth, audit refuses to write state. The refusals keep the seams clean.