VALUE.AL

← All writing

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.

INTERNET clients · providers · TLS REMIT one PostgreSQL schema per service · one trace per movement · no card data anywhere Client app web · mobile Payment provider tokenises cards PCI scope ends here Funding Requested Submitted Settled closed edges — no double settle idempotency_keys — replay · 409 · 422 deposit + outbox row — one transaction relay — SKIP LOCKED, publisher confirms webhook verified before parsing (Countersign) PSP routed by currency, then observed health RabbitMQ topic exchange outbox → relay → publish traceparent in headers Ledger inbox — message id, same tx journal — balanced entries balance = Σ postings Reconciliation movements from events statement ⇄ reference only exceptions — humans resolve stuck sweep POST /deposits Idempotency-Key charge Accepted · Rejected · Unavailable webhook — HMAC over raw bytes per-provider secret · 5-min window settled funding.# month-end statement (CSV) — matched on the provider reference, differences become exceptions
One deposit through Remit, end to end. The highlighted path is the money-carrying event; dashed lines cross a trust boundary. Every box maps to a DORA article below.

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:

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:

  1. A C4 model in the repository, with the PCI boundary drawn (Art. 8).
  2. ADRs for the money path: idempotency, state machines, outbox, ledger, provider boundary, secrets (Art. 9, 13).
  3. Telemetry with trace context across services, and SLOs with alerts — including the two zero-tolerance ones (Art. 10).
  4. Migrations as release steps; backups with written RTO/RPO (Art. 11, 12).
  5. Incident classification queries written before the incident (Art. 17–19).
  6. Integration tests against real dependencies and a STRIDE threat model with residuals (Art. 24–27).
  7. 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.