DORA for a .NET payments backend: what an architect has to be able to answer
Since 17 January 2025, every EU-regulated financial entity — banks, investment firms, brokers, payment and e-money institutions, crypto-asset service providers and the ICT providers that are critical to them — has been subject to Regulation (EU) 2022/2554, the Digital Operational Resilience Act. Most of the writing about it is for compliance teams. This is for the person who has to design the system the compliance team will be asked about.
DORA does not tell you how to build software. It tells you what you must be able to demonstrate about the software you built: that you know what you run, that you would notice it breaking, that you could tell the regulator how badly and how soon, that you tested it before it broke, and that you know which third parties it depends on and what happens when they fail. Those are architecture questions. Below is each one, mapped to the component that answers it in a typical .NET payments backend — deposits and withdrawals through payment providers, a ledger, reconciliation — and to the artefact a supervisor would accept as evidence.
I'll use Remit, a reference money-movement backend I built for exactly this purpose, as the worked example. It is not a product; it is the shape of the answers. Nothing here is legal advice — it is an engineer's reading of what the text demands of a design.
The five pillars, in one table
| DORA chapter | What it asks | The component that answers it |
|---|---|---|
| II — ICT risk management (Art. 5–16) | Know your assets, protect them, detect failures, recover, learn | Asset inventory (a C4 model), state machines and idempotency, health and telemetry, backups and migrations discipline, ADRs |
| III — ICT-related incidents (Art. 17–23) | Detect, classify, report on a clock | SLOs with burn-rate alerts, an incident classification worked out before the incident, traces that reconstruct what happened |
| IV — Resilience testing (Art. 24–27) | Test the whole thing, at least yearly; TLPT every three years for significant entities | Integration tests against real dependencies, a threat model, chaos on the provider boundary |
| V — Third-party risk (Art. 28–44) | Register every ICT provider, contract for exit and audit, don't depend on one | The provider boundary with routing and fall-through, a register that is generated not hand-maintained, reconciliation as the independent check |
| VI — Information sharing (Art. 45) | Share threat intelligence | Out of scope for architecture; note it and move on |
The rest of this piece goes down the table.
Chapter II — ICT risk management
Art. 8, identification: "identify, classify and adequately document all ICT supported business functions, roles and responsibilities, the information assets and ICT assets supporting them, and their dependencies."
The honest translation: you need an inventory that is true, which means it has to be derived from the system rather than typed into a spreadsheet once. A C4 model kept in the repository and reviewed in pull requests is the smallest thing that stays true. Remit's context and container diagrams are Mermaid in markdown for that reason: they change in the same commit as the code, and the PCI scope boundary is drawn on them so "where is card data" has a one-picture answer (nowhere — providers tokenise, the system holds references).
Art. 9, protection and prevention: policies that "ensure the resilience, continuity and availability of ICT systems… and maintain high standards of availability, authenticity, integrity and confidentiality of data."
Integrity is the word that does the work in a payments system, and it is an architectural property, not a policy. Three decisions carry it:
- Money moves through explicit state machines. A deposit is
Requested → SubmittedToPsp → Settled | Failedwith the allowed edges in one table; anything else throws. A duplicate "settled" from a provider cannot settle twice because the edge is closed (ADR-0002). - Every unsafe request carries an idempotency key, and retries replay the original response rather than re-executing. Client retries, provider retries and broker redeliveries are the three ways a system produces a second deposit; the key, the state machine and an inbox at the consumer close each one.
- The ledger is double-entry and balanced by construction. A journal entry that does not balance cannot be created; corrections are new entries; balances are derived, never stored (ADR-0004). When a supervisor asks how you know a balance is right, the answer is "sum the postings" — not "trust the column."
Authenticity of inbound data is the webhook problem: a provider's "your deposit settled" must be verified over the raw bytes with that provider's own secret before it is parsed, with a replay window (ADR-0006). I wrote a separate piece on why that is two signing problems, not one.
Art. 10, detection: "mechanisms to promptly detect anomalous activities… and to identify all potential material single points of failure."
Detection is telemetry plus a definition of "anomalous." The telemetry is OpenTelemetry in every service — request duration, consumer lag, the age of the oldest unpublished event — with trace context carried through the message broker so one trace shows a deposit from HTTP request to ledger posting (ADR-0007). The definition is a set of SLOs with burn-rate alerts (docs/operations/slo.md). Two of Remit's five SLOs have no error budget on purpose: no movement stuck without an exception and no negative wallet balance. They are correctness properties dressed as SLOs so they are measured and paged like one. That is what "promptly detect" looks like in code.
The single point of failure in a payments backend is almost always the provider. Art. 10 wants you to have identified it; Chapter V wants you to have designed around it (below).
Art. 11 and 12, response, recovery and backup: continuity policies, recovery objectives, backup "tested periodically," restoration that does not "jeopardise the security of the network."
Two architectural answers. First, schema changes are a release step, not a boot step: migrations run as pre-upgrade jobs and a failed migration fails the release with the old version still running (ADR-0008). Second, the outbox pattern means nothing is lost between a database commit and a broker publish — a crash there leaves a row to be relayed, not an event that never happened (ADR-0003). Recovery objectives themselves (RTO/RPO) come from the database's backup configuration and the broker's persistence — in Remit's Bicep, 7-day PITR on PostgreSQL Flexible Server and a persistent volume for RabbitMQ. Write the numbers down; the regulation asks for them by name.
Art. 13, learning and evolving; Art. 16's simplified framework for smaller entities.
Learning is post-incident review that changes the system. The cheapest durable form is the architecture decision record: each one says what was decided, what was rejected and why, and what it costs. Nine of them are the spine of Remit's documentation. Art. 16 matters for small firms: the simplified framework still requires identification, protection, detection, continuity and backup — it drops formality, not substance.
Chapter III — incident management
Art. 17–19: an incident process; classification by clients affected, duration, geographic spread, data losses, criticality and economic impact; and reporting to the competent authority — initial notification within 4 hours of classification (and 24 hours of detection), an intermediate report within 72 hours, a final report within one month.
The architectural point most teams miss: classification has to be possible from the data you already collect, in the first hour. "How many clients were affected" is a query over the ledger and the movements table; "how long" is a trace; "economic impact" is a sum. If those questions need a data engineer, the 4-hour clock is already lost.
Remit's threat model and SLO document together give a classification you can run:
| Question the RTS asks | Where the answer is |
|---|---|
| Clients affected | reconciliation.movements by status and period; ledger.postings by account |
| Duration | The SLO burn timeline; traces of the failing path |
| Data loss | The outbox and inbox tables — a gap between sent and processed is the loss, measured |
| Critical service affected | Deposits are the one journey; withdrawals second; everything else is derived |
| Economic impact | Sum of amounts in the affected movements, by currency |
Design the queries before the incident; keep them in the repository next to the SLOs.
Chapter IV — resilience testing
Art. 24–25: a testing programme, at least yearly, on all ICT systems supporting critical functions; Art. 26–27: threat-led penetration testing every three years for significant entities.
Unit tests do not satisfy this; tests against the real dependencies do. Remit's suite runs against real PostgreSQL and RabbitMQ in containers, including one end-to-end test that hosts two services on the same infrastructure and follows a deposit from request through signed webhook, relay, broker and consumer to the wallet balance. The router's tests are chaos on the provider boundary in miniature: a provider that throws, one that is down, one that rejects — each with the expected fall-through.
The threat model is the other half. A STRIDE pass over the deposit flow (docs/security/threat-model-deposit-flow.md) lists, for every threat, the control that exists and the residual — and ends in the five fixes to do first. That document is what a TLPT provider will ask for on day one; having it before they arrive is the difference between a test of your system and a test of your ability to explain your system.
Chapter V — ICT third-party risk
This is the chapter payments architects should read in full, because the payment provider is an ICT third-party service provider under DORA, and a critical one.
Art. 28(3), the register of information: every contractual arrangement with an ICT third-party provider, with the functions it supports and whether they are critical.
The register is a document, but its truth comes from the architecture. If every provider is
behind one interface and configured in one place — Remit's IPaymentProvider and the
Psp:Providers section — the register can be generated from configuration and reviewed
against the contract folder, instead of maintained by hand and drifting. Include the broker,
the cloud platform, the observability backend and the container registry: they are ICT
providers too.
Art. 28(2) and 29, concentration risk: "not overly reliant on a single ICT third-party service provider"; assess substitutability.
This is the architectural core of the chapter. A provider boundary with exactly three outcomes — accepted, rejected, unavailable — and a router that ranks providers by observed health and falls through outages is what "not overly reliant" looks like in code (ADR-0006). The distinction between rejected (the provider answered; its answer stands) and unavailable (try the next one) is the whole routing rule, and it is the difference between a provider outage being an incident and being a log line.
Substitutability has a test: can a new provider be onboarded by adding an adapter and a configuration block, with no change to the state machines, the ledger or reconciliation? If yes, the exit plan Art. 28(8) asks for is a sprint, not a programme.
Art. 30, contractual provisions: service levels, data location, audit and access rights, termination and exit, "full service descriptions."
Not architecture — but the architect should supply two inputs to whoever negotiates: the webhook contract (format, signing scheme, replay tolerance, retry behaviour on non-2xx) and the statement contract (format, period, delivery). Both are what reconciliation depends on, and reconciliation is your only independent check on the provider.
Reconciliation as the control DORA does not name but assumes. Every provider relationship needs a comparison that does not trust the provider's events: their statement against your record, matched on their reference, with differences raised as exceptions that a person resolves with a written reason and that the system never auto-fixes (ADR-0009). It is also the mechanism that finds the failure modes nothing else can — a lost webhook, a crash between two commits, a settlement you never asked for. When a supervisor asks how you would know a provider had gone wrong, this is the answer.
What this looks like as a checklist
For an architect walking into a DORA conversation about a .NET payments estate, the artefacts to have on the table:
- A C4 model in the repository, with the PCI boundary drawn (Art. 8).
- ADRs for the money path: idempotency, state machines, outbox, ledger, provider boundary, secrets (Art. 9, 13).
- Telemetry with trace context across services, and SLOs with alerts — including the two zero-tolerance ones (Art. 10).
- Migrations as release steps; backups with written RTO/RPO (Art. 11, 12).
- Incident classification queries written before the incident (Art. 17–19).
- Integration tests against real dependencies and a STRIDE threat model with residuals (Art. 24–27).
- One provider boundary, health-ranked routing, a generated register, and reconciliation that never auto-fixes (Art. 28–30).
None of these is a compliance artefact bolted on afterwards. Each is the natural output of designing a money-movement system properly and writing the decisions down as you go — which is why the regulation, read as an engineer, is less a burden than a description of the job.
Remit is MIT-licensed at github.com/value-al/Remit. Regulation (EU) 2022/2554 and its RTS/ITS are on EUR-Lex and the ESAs' sites; article numbers above refer to the Regulation itself. This is an engineer's reading, not legal advice.