Common Login Portal

Mock and real adapters behind every port

Ashish Bhagat · 13 Aug 2026

CLP has four external dependencies that matter: the government PKI/SAML identity provider, the Ministry of Labour SOAP API, the Commercial Registry SOAP API, and the Bank Muscat payment gateway. Every one of them has two adapter implementations behind the same port — a mock and a real one — toggled by Spring profile. That decision runs through most of this series, usually in the background of some other problem. This post is about the decision itself.

Four dependencies, two versions each

It's a decision that costs something up front and pays it back continuously rather than once. Every port needs two working implementations instead of one, which is genuinely more code to write and more code to keep in sync as the port's contract evolves. What that buys back is a development and testing loop that doesn't stall every time a government system is down, a bank sandbox is unreachable, or credentials for a lower environment haven't been provisioned yet — which, on a project integrating with four separate external systems across four environments, is not a rare condition to design around. It's closer to the default state.

Government systems being unavailable wasn't a hypothetical risk to plan around abstractly — it was the normal operating condition of building against them. QA and UAT needed to run complete, end-to-end flows — registration, verification, payment, sign-in — on a schedule that had nothing to do with whether the Ministry of Labour's API happened to be reachable that day. Building a mock adapter for each of the four dependencies, matching the same port interface as its real counterpart, meant the rest of the system genuinely couldn't tell the difference at the call site. Switch a Spring profile, and CLP either talks to Keycloak standing in for the government IdP or the ministry's actual identity provider; either a mock labour-status response or MOL's real SOAP API; either a stubbed payment confirmation or an actual encrypted round trip through Bank Muscat.

What a port actually makes possible

It's a small thing to say and an easy thing to get wrong in practice: a mock adapter is only useful if it's held to the same interface as the real one, with no special-casing anywhere upstream. The moment a controller or a command checks "are we in mock mode" and branches its own behavior, the port stops being a real abstraction and starts being a leaky one — which defeats the entire purpose of building it this way in the first place.

None of this works without the domain and application layers depending only on an interface — a port — and never on which adapter happens to be behind it at runtime. That's the same hexagonal architecture boundary this series has come back to more than once: the code that decides what a valid payment or a verified organization looks like has no idea, and no way to find out, whether it's talking to a mock or the real Bank Muscat gateway. Swapping one for the other is a configuration change, not a code change, precisely because nothing on the calling side was ever allowed to depend on adapter-specific detail in the first place.

The mechanism behind the toggle is ordinary Spring: each adapter is a bean annotated for a specific profile, and activating a profile at startup decides which implementation of a given port actually gets wired in. Nothing exotic — the same mechanism that later had to be reworked when a client requirement meant the payment adapter and the SAML adapter needed to be selectable independently instead of moving together, which is its own story. What matters isn't the mechanism, it's that the mechanism is boring and well-understood, which is exactly what you want for something that decides whether a given environment is talking to a real government system or not.

Building both adapters also changes project sequencing in a way that's easy to undervalue until you've worked without it. Government API access, bank gateway credentials, and IdP metadata all take real time to arrange — none of them are things a solo engineer can just spin up on demand the way a local database or a message queue can be. Having a mock adapter behind the same port from day one means the application layer, the controllers, the frontend flows that depend on a given integration can all be built and tested well before the real credentials and access exist, instead of the whole project waiting on procurement and government coordination before any of that code can be written at all.

What mocking can't buy you

The honest version of this story isn't that mock adapters solved the availability problem and that was the end of it. Two of the more interesting bugs in this series happened specifically at the boundary a mock adapter can't reach, because a mock is only as faithful as the assumptions someone wrote into it, and some failure modes only exist in the real system's own idiosyncrasies. The Java 17 certificate-parsing failure only showed up against the ministry's actual signing certificate — Keycloak, standing in for the IdP everywhere else, was never going to produce that specific encoding quirk, because nothing about it needed to resemble real government PKI tooling to work as a stand-in for sign-in flows. The GET that silently dropped a POST body is even sharper: that's a browser navigating to a real third-party URL and finding out what that URL actually does with the request. A payment mock, by construction, never involves a browser navigating anywhere at all.

Both bugs passed every test that ran against the mock. Neither bug was the mock's fault — a mock's entire job is to agree with whatever assumptions were built into it, and both of those assumptions were simply wrong about how the real dependency behaved. That's not an argument against mocking. It's a reminder of exactly what a passing test against a mock is actually telling you: that your code does what you told the mock to expect, not that the real system will agree.

What I'd do differently

The pattern is worth keeping — full mock/real adapter pairs behind every port, toggled by profile, is a large part of why QA and UAT stayed usable despite government systems being unreliable. What I'd change is treating "passes against the mock" as a milestone, not a finish line, for any dependency where the real system has its own undocumented behavior — which government PKI and third-party payment gateways both reliably do. That means getting a real or realistically-flawed sample of the actual dependency's behavior into a lower environment as early as it exists, specifically to stress the assumptions the mock was built on, instead of trusting a clean mock all the way until the one environment where the real thing is unavoidable.


More from this series:

AVA not a sequence: a Java 17 SAML cert bug · Bank Muscat Error 10001: a GET that ate a POST