Fintech4 min read

Building PSD2 Open Banking against a specification that kept moving

Hundreds of pages of standard, a national layer still being written, and other banks who were not ready either. The hard part of Open Banking was never the API.

Nikolozi KuridzeLead Software Engineer · Solution & AI Architect

Open Banking is usually explained as "banks expose APIs". That description is accurate and almost useless, because it makes the API sound like the hard part.

I architected and built a complete PSD2-compliant Open Banking system at ProCredit Bank Georgia, from architecture through to production, handling millions of transactions at 99.9% uptime. The API was the easy part. Here is what actually consumed the effort.

You read hundreds of pages, implement it, and it does not work

The PSD2 documentation is enormous. I read it, worked through it, implemented against it — and it did not work.

Not because I had misread it. Because there was a second specification: the national implementation adapted to Georgian legislation. Also very large. And, critically, incomplete in places and updated continuously.

So the ground truth for what I was building existed in two documents, one of which was still being written while I was building against it.

That is a different kind of engineering problem from the one the standard prepares you for. The standard teaches you the contract. It does not prepare you for the contract changing underneath a system that is already live.

Implementing a moving target

My implementation and the refinement of the local specification were happening at the same time. Every clarification on their side was a change on mine. The code was in constant motion, not because the design was wrong but because the definition of correct kept being revised.

Underneath that sat a second source of movement: the business could not settle on a single interpretation. Where the specification left room, somebody had to choose — and while that choice was still open, the code had to accommodate more than one possible future.

The consequence was hotfixes in production. Functionality that had been working would break because something changed on the other side, and it had to be corrected immediately rather than in the next planned release. Not because of a defect I had shipped. Because the environment the code integrated with was not stable yet.

If I were starting again, the single change I would make is to push every point of interpretive uncertainty behind configuration from day one, rather than discovering one at a time which decisions were going to move.

Nobody else was finished either

The part I did not anticipate: the other banks were not ready.

Open Banking only means anything if the institutions on the other end can actually be integrated with. Some had implementations riddled with defects. Some had not finished at all. And coordinating with all of them was my responsibility.

That coordination stretched out, because it was new to them as well. There was no reference implementation to point at, no shared understanding of what "correct" looked like. Every integration was two teams discovering the specification simultaneously and comparing notes.

Which leads to the uncomfortable truth of the whole programme: testing effectively happened in production. Not by choice, and not for lack of a test environment — but because the only place the real counterparties existed was production, and their behaviour was the thing under test.

The day the whole thing went down

We do not deploy at night. Updates go out during the day, deliberately, when people are awake and watching, and there is a rollback path. Things do not break by accident at 3am because nothing happens at 3am.

They broke at a perfectly reasonable hour instead.

We had moved functionality to production and were testing it there — which, per the above, is where that testing had to happen. Open Banking production went down. Minutes, not hours, and the rollback did what it was there for.

The cause was almost disappointing in its ordinariness. A record that existed in the test database, and which needed to exist in production too, never made it. The database identity value had fallen behind what the code expected — so the row the code went looking for was not there, under an ID that had drifted out of alignment between environments.

No dramatic architectural flaw. A seed row and an identity sequence.

The lesson I took from it is not "be more careful". It is that environment parity has to include data, not just schema. We were rigorous about keeping test and production structurally identical and much less rigorous about the reference rows that structure depends on — and structure without its seed data is not actually parity, it just looks like it in a migration diff.

What I would tell someone starting this

Assume the specification will move. In a domain that is being regulated in real time, the document is a snapshot. Build the interpretive decisions as configuration, not as constants, because you will be revising them while the system is live.

Budget for other people's readiness. Your integration timeline is not set by your own delivery. It is set by the least-finished counterparty you are required to connect to, and you will have no authority over them.

Parity means data too. Identical schemas with divergent reference data is a category of failure that hides right up until the moment it takes production down.

Deploy in daylight. The reason our outage lasted minutes is that everyone who could fix it was already at their desk. That is not luck; it is a scheduling decision, and it is nearly free.

PSD2Open Banking.NETIntegrationProduction

Keep reading