Skip to content

Publication

Designing a Universal Adapter for a Domain-Neutral Runtime

An architecture note on separating domain interpretation, runtime-facing contracts and technology bindings while integrating operational systems.

EFL-AN-2025-002 / ARCHITECTURE NOTE

Separating domain interpretation, translation and transport while integrating operational systems.

Abstract

Introducing an application-owned runtime solved one problem in our systems and exposed another. Once operational work no longer depended on the web request that initiated it, the runtime had to communicate with an increasingly varied set of external systems. The first integrations were conventional pieces of application code: HTTP clients, XML builders, database access and file importers. Each implementation was reasonable on its own.

The difficulty became visible later. Some of those components had accumulated responsibilities that had little to do with their underlying technology. An HTTP client might also interpret external identifiers and decide which fields represented authoritative information. A file importer could contain similar decisions independently. Replacing an acquisition mechanism therefore meant recovering domain knowledge that had become embedded in implementation-specific code.

After several integrations had been revised, three areas kept appearing in code reviews and refactoring work: interpretation of the external domain, the common interaction expected by the runtime, and the mechanism used to communicate with a particular technology. Their separation became the basis of what we call our Universal Adapter architecture.

The term universal is intentionally limited. It does not imply that arbitrary systems can be connected without specialized engineering. We use it to describe a stable architectural boundary that allows different domains and technologies to interact with the runtime without requiring the runtime itself to understand each of them.

1. The runtime created an integration problem of its own

Separating operational work from the application request lifecycle allowed the same kind of operation to begin from a browser request, scheduled process, recovery procedure or another system interface. The runtime no longer needed to inherit the lifecycle assumptions of whichever interface happened to start the work.

Looking outward was more complicated.

Operational systems consult registries, transmit documents, receive acknowledgements, read local datasets and interact with services whose behavior and availability are outside the application’s control. We had integrations using JSON over HTTP, XML interfaces, local files and database-backed datasets. Even when two integrations used similar technologies, they could have very different rules for identity, validity and authority.

Our first implementations handled those differences where they appeared. An HTTP integration knew how to call its endpoint and parse the response, but over time it might also learn which fields identified a record, what constituted an acceptable result and how the information should be represented internally. File and database integrations accumulated comparable responsibilities.

For small boundaries this was perfectly workable. We still use direct integrations where introducing another abstraction would make the system harder to understand.

The problem surfaced when one of those boundaries had to move.

2. Our first adapters knew more than their names suggested

We encountered this while working with information maintained by an external authority. The first acquisition mechanism was a remote service, so the integration naturally grew around an HTTP client and the representation returned by that service.

Later, some of the same information needed to remain available locally. The reason was operational rather than stylistic: a temporary outage of a reference service should not disable application capabilities that could safely continue from synchronized data.

The change initially looked straightforward. Instead of requesting the record remotely during every operation, the application could obtain it from a local dataset.

Implementation showed that the HTTP integration had accumulated considerably more responsibility than its name suggested. It knew which external fields mattered, how identifiers were normalized, which values represented authoritative identity and how missing information should be handled. Those decisions were required whether the record arrived over HTTP or from synchronized local storage.

Similar cases appeared elsewhere. XML builders contained assumptions about business validity. Database repositories occasionally knew details about external identity that were unrelated to storage. Some API clients interpreted remote results deeply enough to decide whether an operation should be considered complete.

These decisions had usually entered the code for practical reasons. Their location became a problem only when another implementation needed the same knowledge.

3. Acquisition mechanisms were a poor architectural boundary

At that stage, our integration code was still organized largely around acquisition mechanisms. As implementations accumulated, we found ourselves repeating decisions about identifiers, authority, validation and internal representation in places originally created to handle HTTP, files or database access.

Consider an external registry available through a remote API and later through a periodically distributed dataset. The acquisition mechanism changes substantially, but the identifier still represents the same thing and the same authority remains responsible for the underlying information.

The opposite case also occurs frequently. Two unrelated services may both exchange JSON over HTTPS while performing operations with entirely different consequences. Knowing that both are REST APIs tells us almost nothing about how their information should be treated after it enters the application.

Separating these responsibilities made later changes easier to locate. A new acquisition mechanism could often reuse existing domain interpretation. Changes in an external domain could usually be handled without rewriting unrelated transport code.

This also exposed cases where domain code had inherited details such as HTTP headers, XML namespaces, filesystem paths or pagination rules simply because those details were necessary to obtain the information. Some of that coupling was harmless. Other cases became expensive once a second implementation appeared.

4. Three responsibilities began appearing repeatedly

After several integrations had been revised, the same three areas kept appearing in code reviews and refactoring work. Some code interpreted the external domain. Some established the common interaction expected by the runtime. The rest existed because a particular protocol, storage mechanism or external interface required it.

We eventually gave those responsibilities explicit names:

Domain Connector
       ↓
Universal Adapter Kernel
       ↓
Technology Binding

The Domain Connector contains the interpretation required by a particular integration domain. The Universal Adapter Kernel defines the common architectural contract presented toward the runtime. The Technology Binding handles communication with a particular technology or external interface.

The diagram is deliberately simple. Real integrations contain authentication, error handling, configuration, persistence and other concerns that do not always fit neatly into three boxes. We use the separation primarily to decide where knowledge should accumulate.

Information can also move in either direction. Some adapters bring external information into an operational system; others carry internally produced information toward a destination. Several do both during different stages of an operation.

Code reviews became simpler once these responsibilities had names. A transport replacement that unexpectedly required changes to identifier interpretation was worth investigating, as was a new connector that forced unrelated changes into the common contract. Sometimes those changes were justified; often they exposed assumptions left behind by an earlier implementation.

5. Keeping the common layer small

The central adapter layer was the easiest place to overdesign.

Several concepts recur across integrations regardless of whether information arrives through HTTP, SQL, files or another mechanism. There is usually a participant or endpoint, something crossing the boundary, some form of identity and a set of capabilities available to the interaction. Input may require validation or translation, while the attempt itself produces information about its outcome.

Those recurring concepts are useful because the runtime can interact with them without learning why a particular field matters to a fiscal system, operational platform or another external domain.

The temptation was to promote new concepts into the common layer whenever an integration needed them. We became increasingly cautious about doing that. A contract that grows by absorbing every exceptional requirement eventually becomes a catalogue of the systems connected to it.

In several cases we deliberately left specialized behavior in a connector even when moving it into the common layer would have removed a small amount of duplicated code. Duplication in those cases was easier to manage than an architectural contract containing assumptions that only one domain understood.

We still revisit those decisions. A concern that looks specialized in the first implementation sometimes appears independently in later integrations and proves to be genuinely common. It is easier to promote a concept after observing that recurrence than to remove it after multiple implementations have started depending on it.

6. Domain interpretation remained explicit

Removing transport concerns did not reduce the amount of domain-specific integration work. In some cases it made that work more visible.

An identifier received as a string illustrates the issue. A technology binding may only need to know how the value is encoded and where it appears in an external representation. The connector may need to determine whether leading zeros matter, what kind of entity the identifier represents, whether an external authority owns its canonical form and whether syntactic validity is sufficient for the operation being performed.

Those are decisions we do not want the runtime making merely because it coordinates the operation.

The connector therefore translates an external representation into the contract expected at the runtime boundary while retaining whatever domain meaning is required for that translation. This becomes particularly important when two systems use similar names for concepts that are not actually equivalent.

We have encountered integrations where mapping id to id or status to status would have produced structurally valid data and incorrect semantics. A generic field mapper cannot resolve that problem. Someone has to know what the external system means, and we prefer that knowledge to have an explicit architectural location.

7. Technology bindings became narrower

The technology-facing part of the adapter became easier to test as its responsibilities narrowed.

An HTTP binding constructs requests, applies transport configuration, communicates with the endpoint and records what can be established about the exchange. XML implementations deal with serialization and parsing. File bindings handle file acquisition and access. Database bindings have connection and query concerns.

None of this makes the technology layer trivial. Certificate handling, authentication, encoding, timeouts, pagination and protocol-specific failures have produced some of the more difficult integration defects we have encountered.

The boundary becomes interesting when technology-specific information has operational significance. A remote API may return a code that the binding must observe but whose meaning depends on the external domain. We generally preserve enough of that information for the connector to interpret it rather than making the HTTP implementation responsible for the final operational decision.

There are cases where validation, identity or error classification can reasonably exist at more than one layer. Forcing every concern into a theoretically perfect location usually creates more ceremony than clarity. The three-part model helps us ask where a responsibility belongs; it does not guarantee that every answer will be obvious.

8. What we eventually meant by “universal”

During the first iterations, universal was an easy word to misuse.

Some early abstractions were flexible because they accepted arrays of different shapes, dynamic options and adapter-specific configuration. They could accommodate many integrations, but callers still needed knowledge that supposedly belonged behind the adapter. Adding another accepted input shape did little to improve the boundary.

We gradually became less interested in how many forms the abstraction could accept and more interested in whether the runtime-facing contract remained understandable as new integrations were added.

That changed how we evaluated the design. The runtime should depend on the capability exposed by an adapter rather than on whether the implementation happens to use REST, SQL, XML, a filesystem or another mechanism.

This led us to a formulation we still use when reviewing adapter boundaries:

The adapter recognizes interfaces, not technologies.

The statement describes the runtime-facing side of the architecture. Technology still matters enormously inside an implementation, and domain expertise remains necessary inside a connector. We are not claiming that those differences disappear.

A new technology should simply have fewer reasons to alter the runtime’s understanding of the operation.

9. One domain can have more than one acquisition mechanism

The separation became useful when we revisited externally maintained reference data.

Some operations benefit from consulting the authority directly because current information matters at that moment. Other operations can safely use a synchronized local representation, particularly when the underlying information changes slowly and temporary loss of connectivity would otherwise disable useful application behavior.

                    ┌── Remote service binding
Domain Connector ───┤
                    └── Local dataset binding

The local and remote representations are not necessarily equivalent in every respect. A synchronized dataset has an acquisition time and may expose a version or publication date. A remote service can provide freshness or authority signals that do not exist in the local copy.

We preserve those differences when an operation needs them.

This mattered because we had already encountered the temporal side of local reference data in our continuity work. A record can be sufficiently current for one operation and too old for another. Historical records introduce another concern because information captured when an operation occurred should not automatically change merely because the reference dataset is refreshed later.

Keeping domain interpretation above the acquisition mechanism gave us somewhere to make those decisions without placing them in the file reader or HTTP client.

10. Some concerns cross the layers

Authentication is one example. A certificate can be required by the transport while the identity represented by that certificate has significance elsewhere in the operation. Pagination normally belongs to the external interface until ordering or consistency guarantees affect the interpretation of the resulting dataset. Rate limits are an API concern until they begin constraining whether an operational process can finish within its required window.

We have stopped treating these cases as failures of the architecture.

Instead, they require an explicit decision about how far the information needs to travel. Sometimes a capability or constraint belongs in the adapter contract. Sometimes the connector can handle it locally. There are also cases where the runtime genuinely needs to know that an operation cannot continue under a particular external condition.

The same pragmatism applies to deciding whether an adapter is warranted at all. A small internal service with a stable interface may be clearer as a direct application dependency. We do not introduce this architecture merely because a network call or file exists.

It becomes useful when enough domains or technologies cross the same operational boundary that their individual assumptions would otherwise begin shaping that boundary.

11. Large common contracts created another kind of coupling

Keeping the runtime free of technology-specific details did not prevent us from creating overly broad contracts.

As adapters accumulated, it was tempting to define interfaces capable of representing every operation supported by a connected platform. That produced abstractions with optional capabilities, configuration and states that many consumers never used.

We found smaller contracts easier to reason about. An operation can depend on the capability it actually requires while the connector and technology binding retain additional behavior that is irrelevant to that particular interaction.

This sometimes means that one external system participates through several adapter capabilities rather than through a single interface representing the entire platform. It produces more interfaces and less impressive diagrams, but dependencies become easier to inspect.

Testing also becomes more specific. We can ask whether an implementation satisfies the contract required by a particular operation without expecting unrelated implementations to behave identically in areas the contract never standardized.

As more than one implementation began satisfying the same contracts, however, another limitation appeared. Matching the interface was useful, but increasingly it was not enough.

12. Multiple implementations made the contract harder to trust

The first adapter implementations could be reviewed largely in isolation. That became less satisfactory once different connectors and technology bindings claimed to satisfy the same runtime-facing contracts.

Matching method signatures told us relatively little about whether two implementations preserved identity, represented incomplete outcomes consistently or handled equivalent input with compatible semantics.

We began keeping reproducible cases around some of these boundaries. They were initially practical test assets: known inputs, expected outcomes and examples of behavior that had previously caused integration defects. Over time they also gave us a more useful way to discuss whether implementations were conforming to the same contract.

This became particularly relevant when a remote and local implementation represented the same domain information. Both could satisfy the same interface while differing in normalization, treatment of missing fields or representation of freshness. Ordinary unit tests around each implementation did not necessarily expose those differences.

The work later informed our engineering standards for contract validation and reproducibility. At the adapter level, its immediate purpose was more practical: an interface declaration was no longer enough evidence for us to assume that two implementations behaved equivalently where the runtime depended on them.

Document record

Document ID: EFL-AN-2025-002
Title: Designing a Universal Adapter for a Domain-Neutral Runtime
Document type: Architecture Note
Category: Architecture Notes
Publication year: 2025
Institution: EventFlow Labs
Language: English
Revision: 1.0