The Approach
The architecture keeps three jobs cleanly separated: ingestion, a normalized core, and delivery. The ingestion layer pulls the DATEX II publications from the official sources, reads them as a stream rather than loading whole documents into memory, validates them against the schema, and translates them into a lean domain model of its own. The standard therefore stops at the system boundary — everything behind it works with clear, self-defined concepts.
The data backbone is a scalable .NET backend. The choice follows the nature of the work: schema-driven XML processed continuously. Static typing maps a model-driven format directly, the XML tooling supports streaming reads of large snapshots at a flat memory profile, and long-running background services with scheduled runs are first-class citizens. Ingestion and the delivery API scale independently, because their load profiles have nothing in common: ingestion runs in bursts dictated by the sources, the API follows user behavior.
Continuous market price updates run on their own cadence alongside the inventory data. Each run is compared against the last known state, so only genuine changes trigger downstream work — writes, cache invalidation, refreshing the map layer. Every record carries its origin and a timestamp with it. That is not cosmetic: it is the only way to show honestly in the app how current a value is, and to trace a complaint back to a specific source and point in time instead of searching the whole system.
On the client side, iOS and Android do not speak DATEX II — they speak a deliberately narrow API. Map queries are viewport-based, backed by a spatial index, and aggregated according to zoom level, so the amount of data transferred is tied to what is visible rather than to the size of the overall dataset. Real-time data in a mobile app therefore does not mean pushing everything to the device; it means serving exactly what each view needs, from prepared read models that still answer when an upstream source happens to be unreachable.