Every operation of a serious system has to be observable and provable: who did what, with what result, what failed, and whether it can be replayed. Often this is the law, for example DORA, SOX or MiFID II. With algebraic effects it is almost trivial. Every operation is typed data, and it reaches the world only through a handler. The audit is one more handler around it, and the business code does not change.
None of these laws mentions effects. Each one asks for something that, in software, comes down to knowing and controlling what the code does. The right column says what that is in code terms.
| Law | Provision | What it asks | In code terms |
|---|---|---|---|
| DORA Regulation (EU) 2022/2554 | Art. 8 | Identify, classify and document ICT-supported business functions, information and ICT assets, and the links and dependencies between them. | Which code reaches which systems, written down and kept current. |
| Art. 9(4) | Limit access to information and ICT assets to what legitimate, approved functions require. | A function touches only what it is allowed to touch. | |
| Art. 28(3) | Keep a register of all contractual arrangements for ICT services from third-party providers. | Which modules depend on which external providers. | |
| PSD2 Directive (EU) 2015/2366 | Art. 95 | A framework of risk-mitigation measures and control mechanisms for the operational and security risks of payment services, with an annual assessment of how well they work. | Controls on every payment operation, and evidence that they ran. |
| MiFID II Directive 2014/65/EU | Art. 16(6), 16(7) | Keep records of all services, activities and transactions, sufficient for the supervisor to do its job; record the communications that lead to transactions, and keep them five years, up to seven if asked. | Every operation recorded with its result, complete enough to reconstruct what happened. |
| MiFIR Regulation (EU) 600/2014 | Art. 25 | Keep the relevant data on all orders and transactions at the supervisor's disposal for five years. | A trail of orders and transactions, produced by the code that executes them. |
| GDPR Regulation (EU) 2016/679 | Art. 5(2), 25, 30, 32 | Accountability: be able to demonstrate compliance. Data protection by design and by default. Records of processing activities. Security of processing. | Personal data reached only through declared operations, masked where it leaves, every processing recorded. |
| AML Directive Directive (EU) 2015/849 | Art. 40 | Keep customer due-diligence data and transaction records for five years. Its successor, the AML Regulation (EU) 2024/1624, keeps the duty. | A complete, durable trail of transactions, produced by the code that makes them. |
| AI Act Regulation (EU) 2024/1689 | Art. 12, 14; Annex III 5(b) | For high-risk AI systems, including creditworthiness assessment: automatic logging of events over the system's life, and effective human oversight, up to stopping the system. | Every action of an AI system or agent logged, checkable, and refusable. |
| NIS2 Directive (EU) 2022/2555 | Art. 21 | Cybersecurity risk-management measures, supply-chain security among them. | Known, bounded dependencies, and controlled access to the outside world. |
| Sarbanes–Oxley US, Public Law 107–204 | § 404 | Management assesses, and auditors attest to, internal control over financial reporting. | Controls on the operations that produce financial records, provable to an auditor. |
| PCI DSS industry standard | Req. 7, 10 | Restrict access to cardholder data by business need to know. Log and monitor all access to system components and cardholder data. | Card data reached only through declared operations, each access logged. |
The provisions are summarised here. The linked official texts are what applies, and this page is not legal advice. Some obligations depend on the kind of firm and on dates of application.
Imagine a bank whose operations are modelled like blockchain UTXO. Money is a set of coins, and each coin is spent once, atomically, so when two payments race for the same coins only one wins. The ledger is an effect: two operations, as data. The bank is a handler that carries them out against a table.
case class Coin(id: String, owner: String, value: Int)
enum Ledger[+A]:
case Unspent(owner: String) extends Ledger[List[Coin]]
case Spend(in: List[Coin], out: List[Coin]) extends Ledger[Boolean]
import Ledger.*
trait Table: // DynamoDB, say
def unspent(owner: String): List[Coin]
def spend(in: List[Coin], out: List[Coin]): Boolean // atomic, iff no input is spent
def bank[G[_], A](db: Table) = Handler[Ledger, A, A, G](Return(_),
[X] => (op, k) => op match
case Unspent(o) => k(db.unspent(o))
case Spend(in, out) => k(db.spend(in, out)))
A payment knows only its effect. Nothing in it says where the coins live, and nothing in it says it is audited:
def pay(from: String, to: String, sum: Int): Eff[Ledger, Boolean] = for
in <- perform(Unspent(from))
total = in.map(_.value).sum
tx = in.map(_.id).mkString("+")
ok <- if total < sum then Return(false)
else perform(Spend(in, List(Coin(tx + ">0", to, sum), Coin(tx + ">1", from, total - sum))))
yield ok
The audit wraps any handler. It records the intent first, then the result or the failure. This is write-ahead: with no record there is no operation.
enum Event:
case Begin(op: Any)
case End(op: Any, result: Any)
case Fail(op: Any, error: Throwable)
import Event.*
trait AuditLog: // Kafka, say
def append(e: Event): Unit
def read: Iterator[Event]
def audited[F[_], A, B, G[_]](h: Handler[F, A, B, G], log: AuditLog) =
Handler[F, A, B, G](h.ret, [X] => (op, k) =>
log.append(Begin(op)) // write-ahead: no record, no operation
var done = false
try h.eff(op, x => { done = true; log.append(End(op, x)); k(x) })
catch case e: Throwable => { if !done then log.append(Fail(op, e)); throw e })
val system = [A] => (p: Eff[Ledger, A]) => run(handle(audited(bank(db), log))(p))
system(pay("alice", "bob", 30)) // true
Replay needs no database. A handler answers each operation from the log, so a program run again gets the same answers. A program that does something different is caught at the first operation that differs:
def replay[G[_], A](log: AuditLog) =
val ends = log.read.collect { case e: End => e }
Handler[Ledger, A, A, G](Return(_), [X] => (op, k) => (op, ends.next()) match
case (Unspent(o), End(Unspent(p), r: List[Coin] @unchecked)) if o == p => k(r)
case (Spend(i, u), End(Spend(j, v), r: Boolean)) if i == j && u == v => k(r)
case (op, e) => throw IllegalStateException(s"diverged: $op"))
val replayed = [A] => (p: Eff[Ledger, A]) => run(handle(replay(log))(p))
replayed(pay("alice", "bob", 30)) // true, from the log
replayed(pay("alice", "bob", 50)) // diverged: Spend(...)
Neither the payment nor the bank knows about the audit. It is added, or swapped for another, where the system is put together.
The intent is written before the operation runs, and the result or failure after it. The trail is complete because there is no other way to the world.
The log is enough to rerun a program without its database, and to find where a new version behaves differently from the old.
The example runs on the Eff monad from the author's earlier post, and it is the whole mechanism. okay-effects provides it as a library, with more guarantees on top, described below.
In okay, audit is part of the kernel and the types enforce it. A sensitive effect cannot run without its audit, an observer sees every effect, and the build checks the boundary in the bytecode.
| Requirement | Mechanism in okay | Serves |
|---|---|---|
| Know what each part of the system may do | The type lists every effect a program may perform, for example Receipt ! Db + Payments. The compiler holds the program to that list. | DORA 8, 9(4); GDPR 25 |
| Sensitive actions only under control | An effect marked Sensitive, such as payments, cannot be performed or handled outside its audit. A program that tries does not compile. | PSD2 95; SOX 404; PCI DSS 7 |
| A policy at the point of action | An interceptor sees each operation, and can pass it on, change it (cap an amount, normalise an account, mask data) or refuse it. | DORA 9(4); PSD2 95; AI Act 14 |
| A trail made by the action itself | The trail is an effect too. Each operation and its answer is recorded where it happens, and its handler sends the record wherever records are kept. | AI Act 12; AML 40; GDPR 30; PCI DSS 10 |
| Oversight of an AI agent | An observer sees every operation of every effect on its way out of a program, an agent's tool calls included. It can record them, wait for a person, or refuse. | AI Act 12, 14 |
| The dependency inventory | okay-audit checks the compiled bytecode at every build. Business code must reach no network, files, SQL, clock or threads on its own. It lists, by provider, the modules that do. | DORA 8, 28(3); NIS2 21 |
| Test, replay, investigate | The same program runs under test handlers, recording handlers and replaying ones. An incident is replayed from its trail, operation by operation. | DORA's testing and incident duties; GDPR 32 |
The details are in okay-core's docs/intercept.md, and the same guarantee exists in Rust (okay-rust) and TypeScript (okay-ts). What okay gives is evidence and mechanism for your own compliance work. It is not a certification.
You do not rewrite the system. You start where the risk is.
Take payments, customer data or model calls, and make that one an effect with a handler over the code that exists today. Then add its audit and its trail.
Run okay-audit at every build. The report shows which modules reach the outside and how, so the dependency inventory comes from the build, not from a spreadsheet.
Move more of the code onto effects module by module. The same boundaries are where a monolith splits into services. It interoperates with cats, ZIO, fs2 and kyo on Scala, and the design exists for Java, Go, Rust, Python and TypeScript.
Help with this is available, as a team member or as a contract consultant: the author.