Developer Guide
Subdomain type: Core · Grounded in: User Stories TI-1 through TI-6
This page describes the design-time model of the Telemetry Ingestion context. How readings are accepted and routed at runtime — the mechanism that lets new sensor types be added — is left as a decision for whoever builds this model; the domain states only that it must be possible without a coordinated release of every consumer (see the Published Language section below).
At the domain level, this context turns raw sensor input into validated, time-stamped readings, and holds the thresholds those readings are judged against.
| Term | Meaning in this context |
|---|---|
| Signal | A single time-stamped reading from one room (people count, temperature, air quality). |
| Metric | A category of signal, identified by a stable key. |
| Threshold | A bound a metric is expected to stay within, for a building or a room. |
| Reading window | The time range over which signals are aggregated for display. |
We treat each metric as a small aggregate root of its own: an append-only reading that shares a common identity (which building, which room, when) plus its metric-specific values. Signals are conceptually immutable — a correction is a new reading, not an edit of an old one.
classDiagram
class PeopleCountSignal {
<<Aggregate Root>>
+building
+roomId
+timestamp
+peopleCount
}
class TemperatureSignal {
<<Aggregate Root>>
+building
+roomId
+timestamp
+temperature
}
class AirQualitySignal {
<<Aggregate Root>>
+building
+roomId
+timestamp
+pollutants
+aqi
}We chose one aggregate per metric rather than a single generic “reading” so that each metric can carry its own fields and grow its own rules without entangling the others. Whether the set of metrics is fixed or open-ended was left flexible at this stage — the model is meant to accommodate metrics we have not designed yet, the flexibility story TI-5 asks for. The reading window over which signals are later aggregated (story TI-6) is served from this recorded, append-only history.
BuildingThreshold is the one mutable aggregate in this context: the consistency boundary around all of a Building’s configured bounds, with optional per-room overrides modelled as value objects.
classDiagram
class BuildingThreshold {
<<Aggregate Root>>
+buildingId
+Bound temperature
+Bound peopleCount
+Bound airQuality
+RoomThreshold[] rooms
}
class RoomThreshold {
<<Value Object>>
+roomId
+Bound temperature
+Bound peopleCount
+Bound airQuality
}
class Bound {
<<Value Object>>
+min
+max
}
BuildingThreshold "1" *-- "many" RoomThreshold : overrides
BuildingThreshold "1" *-- "many" Bound : building defaults
RoomThreshold "1" *-- "many" Bound : room overridesThe threshold aggregate is built from value objects. A Bound is a min/max pair with no identity — two bounds carrying the same numbers are the same bound — and a RoomThreshold is just a room’s set of overriding bounds. Both are immutable descriptors owned by BuildingThreshold; we change them by replacing them on the aggregate, never by giving them a life of their own. The signals above, by contrast, are aggregate roots rather than value objects: each reading carries its own identity of which room, when.
Accepting a reading is a domain decision, not just a transport check. The intent is that an incoming reading is judged against the model before it is accepted, and that the outcome is a structured verdict the caller can act on — not an exception.
validate(reading):
confirm the reading identifies a building, a room, and a time
confirm the metric-specific values are present and well-formed
return ok, or the list of reasons it was rejectedDesigning validation to return a verdict (rather than throw) is a deliberate choice so that a rejected reading can be reported clearly to its source.
The intended intake path is short: once a reading is validated, it is normalized into a canonical signal and published into the platform so it can flow on to the client; an invalid reading is rejected at the door and never enters the system — the fire-and-forget path story TI-1 asks for, and the immutable, append-only recording story TI-4 relies on.
sequenceDiagram
participant S as Sensor source
participant I as Telemetry Ingestion
participant P as Rest of the platform
S->>I: submit a reading
Note over I: validate against the model
alt accepted
Note over I: normalize into a canonical signal
Note over I: record the signal
I->>P: publish the signal
Note over P: distributed and delivered to the client
else rejected
I-->>S: rejected, with reasons
endFor the Telemetry Distribution context to make sense of readings without bespoke coupling, this context is meant to describe its own metrics — their key, label, unit, and fields — as a small, stable contract.
Metric:
key -- stable identifier, e.g. "temperature"
label -- human-readable name
unit -- optional unit of measure
fields[] -- the shape of a reading of this metricPublishing a self-describing metric is what allows a new metric type to become visible downstream without a coordinated change — an application of the Open Host Service / Published Language idea introduced in Strategic Design.