The Boundary Is the Product: Why Integration Code Must Protect Meaning

min dulle

Hatched by min dulle

Aug 11, 2026

11 min read

78%

0

What if the most dangerous code in your system is the code you call “glue”?

It often looks harmless. A few lines turn a Rails object into an API payload. A short Python script reads records, renames columns, and writes them somewhere else. The code may be too small to deserve a design document, yet it sits at the exact point where one system’s assumptions become another system’s reality.

That is the deeper connection between port and adapter architecture and practical data integration: integration code is not a convenience layer. It is a boundary where meaning, responsibility, and risk are negotiated.

Once you see that, the design question changes. You are no longer asking, “How do I connect these systems efficiently?” You are asking, “What should my system allow an external system to change?”

The Smallest Code Can Carry the Largest Assumptions

Consider a simple feature in an online store. When an order is placed, the application sends shipping details to a carrier’s API. The first implementation may be wonderfully direct:

carrier.create_shipment(
  recipient_name: order.customer.name,
  address_line: order.address.street,
  postal_code: order.address.zip
)

The code is readable, fast to write, and apparently correct. But it quietly embeds a long list of assumptions:

  • The carrier uses the same address model as the application.
  • The carrier’s field names will remain stable.
  • A successful HTTP response means the shipment was actually created.
  • The carrier’s error vocabulary is suitable for the application’s users.
  • The carrier can be called whenever the business operation occurs.
  • The carrier’s identifier can safely become the application’s identifier.

None of these assumptions belongs in the order domain. They belong to the relationship between the order domain and one particular carrier.

The same issue appears in data work. A Python job in a managed data service may read files from object storage, transform a table, and write the result to a catalog or warehouse. The transformation itself may be simple, but it embodies assumptions about schema, null values, timestamps, partitions, encoding, duplicate records, and what constitutes a valid row.

In both cases, the visible task is translation. The actual task is protecting one model from another model’s instability.

Integration is where a system discovers that “the same thing” is rarely represented in the same way twice.

This is why the port and adapter pattern matters beyond object oriented design. A port defines what the application needs in its own language. An adapter translates that need into the language of an external service, database, file format, or managed processing platform. The application owns the port. The outside world is allowed to vary behind the adapter.

The pattern is often presented as a way to make testing easier. That is useful, but incomplete. Its more important function is containing semantic change.

A Port Is a Decision About Ownership

Suppose the application needs to “book a shipment.” A weak interface might mirror the carrier:

carrier_api.create_label(order_id, service_code, account_number)

This makes the external provider’s concepts visible everywhere. A stronger port expresses the application’s intention:

shipping.book(order, destination, delivery_speed)

The difference is not cosmetic. In the first version, the application knows how the carrier thinks. In the second, the adapter is responsible for converting the application’s meaning into carrier specific instructions.

This suggests a practical rule:

A port should describe a business capability, while an adapter should describe an external mechanism.

The distinction becomes even clearer when there are several providers. Imagine a payment system with adapters for two processors. One supports delayed capture, another supports immediate capture. If the port is designed around processor terminology, the application will accumulate conditional logic:

if processor == "ProviderA"
  capture_later
else
  charge_now
end

At that point the adapter has failed. It has not translated the difference. It has exported the difference into the core of the application.

A better design asks what the business actually needs. Perhaps the port exposes authorize_payment and capture_payment, while each adapter implements those operations according to its provider’s capabilities. If a provider cannot support a capability, the adapter returns a deliberate, typed failure. The limitation remains visible, but it is located where it belongs.

The same principle applies to data pipelines. A raw source may call a field cust_id, another may call it client_number, and a third may bury the value inside a nested document. The internal data model should not simply inherit whichever naming convention arrived first. A translation step should define a stable concept such as customer_id, along with its type, requiredness, and identity rules.

This is not needless abstraction. It is an ownership decision. The internal model says, “This is what a customer identifier means here.” The adapters say, “Here is how each source provides evidence for that meaning.”

The Adapter Is an Executable Policy, Not a Wrapper

A common mistake is to treat an adapter as a thin wrapper around an SDK. Thinness is not the goal. Clarity of responsibility is the goal.

A real adapter may need to perform several kinds of translation:

  1. Shape translation: Convert fields, nesting, types, and formats.
  2. Semantic translation: Map external statuses and errors into internal concepts.
  3. Temporal translation: Handle retries, asynchronous completion, polling, and late arriving data.
  4. Reliability translation: Apply timeouts, idempotency, rate limits, and circuit breaking.
  5. Governance translation: Redact sensitive values, record lineage, and enforce retention rules.

Imagine a carrier API that returns label_created, in_transit, exception, and delivered. The application may need only ready, moving, problem, and complete. The adapter should perform that mapping. If every controller and background job interprets carrier statuses independently, the system will eventually contain three definitions of “delivered.”

Now consider a data transformation. A source system may send an empty string for an unknown date. The destination may reject empty strings but accept null values. A naive script can replace the empty string with null and move on. A responsible adapter asks additional questions: Was the field absent, explicitly unknown, malformed, or accidentally truncated? Those states may have different operational and analytical consequences.

This is why code samples for managed data services are valuable as starting points but dangerous as architecture. A sample can demonstrate how to read a source, apply a transformation, and write an output. It cannot decide what your organization means by a duplicate, a valid timestamp, a trustworthy customer record, or a recoverable failure.

Examples teach syntax. Adapters must encode policy.

The more invisible a translation becomes, the more deliberately its policy must be designed.

A useful way to evaluate integration code is to ask four questions:

  • What external assumptions does this code absorb?
  • What internal meaning does it protect?
  • What failures can it distinguish?
  • What evidence will remain when something goes wrong?

If the code cannot answer these questions, it is probably not an adapter yet. It is a leak.

The Boundary Needs Two Contracts, Not One

Teams often focus on the external contract: the API schema, the file format, the authentication method, or the managed service interface. That contract matters, but it is only half of the design.

Every integration has two contracts:

  • The external contract describes what the outside system sends or accepts.
  • The internal contract describes what your system promises to the rest of itself.

The adapter exists between them. Its job is not merely to satisfy the external contract. Its job is to prevent the external contract from becoming the internal one by accident.

For example, an API may return a numeric amount as a string because that is how its JSON serializer works. The internal contract may require a money type with currency and exact decimal behavior. The adapter should convert the value at the boundary, not let string arithmetic spread through the application.

A data job may receive a column whose meaning changes depending on the source system. One source uses status = 1 for active, another uses status = 1 for archived. A shared downstream table cannot solve this ambiguity after the fact. The source adapter must preserve provenance or map the value using source specific rules before the records enter the common model.

This two contract model leads to a powerful architectural test:

Can the internal system be understood without reading the external provider’s documentation?

If the answer is no, the boundary is too permeable. Provider documentation should be necessary for maintaining the adapter, not for understanding the application’s core behavior.

The test also works for pipelines. A transformation should make the destination dataset intelligible without requiring analysts to remember every quirk of the source export. Column definitions, quality rules, source identifiers, and transformation logic should form a coherent internal contract.

Design for the Failure You Will Need to Explain

Integration failures are rarely confined to the moment they occur. A request times out, but the external system may still have processed it. A batch job fails after writing half its output. A schema changes silently, producing records that are technically valid but semantically wrong.

The crucial design question is not, “Can this integration succeed?” It is, “Can we determine what happened when success is ambiguous?”

For an API adapter, that may require:

  • An idempotency key derived from a stable business operation.
  • A clear distinction between rejected, unknown, and completed requests.
  • Correlation identifiers stored with the local operation.
  • Retry rules based on error categories rather than generic optimism.
  • A reconciliation process for operations that remain uncertain.

For a data adapter, it may require:

  • A raw landing area that preserves the original input.
  • Schema validation before records enter trusted tables.
  • Quarantine for malformed or suspicious records.
  • Counts and checksums for each processing stage.
  • Run identifiers and source timestamps for lineage.
  • Reprocessing that does not create duplicate business facts.

These practices may seem like operational details, but they are architectural consequences of translation. Once data or commands cross a boundary, reversibility becomes expensive. The adapter should therefore preserve enough context to reconstruct the decision it made.

Think of an adapter as a customs checkpoint. It does not merely open a gate. It checks what is entering, converts what must be converted, records where it came from, and has a procedure for items that cannot be classified. A system with no such checkpoint is not frictionless. It is simply importing uncertainty.

A Practical Framework: Meaning, Mechanics, and Memory

A useful way to design any integration is to separate three layers of thought.

1. Meaning

What business fact or capability is crossing the boundary? Define it in language that makes sense inside your system. Avoid beginning with endpoint names, SDK methods, or source column names.

For an order system, the meaning might be “request delivery for a confirmed order.” For a data pipeline, it might be “publish a canonical customer record that downstream systems may use for reporting.”

2. Mechanics

What does the external system require? Identify authentication, payload shape, pagination, file layout, partitioning, rate limits, execution context, and service specific behavior. Put these details in the adapter and keep them out of the core model wherever possible.

3. Memory

What must the system remember to explain, retry, reconcile, or audit the operation later? Store external identifiers, source versions, run metadata, transformation results, and meaningful failure reasons.

Many integrations handle meaning and mechanics but neglect memory. They work in the happy path, then become impossible to debug. Others preserve logs but never define meaning, leaving a large historical record of inconsistent interpretations.

The strongest integrations make all three explicit.

Key Takeaways

  • Design the internal port first. Express the business capability or canonical data concept in your own language before studying the provider’s interface.
  • Treat adapters as policy boundaries. Put mapping, validation, status interpretation, retries, privacy rules, and provider specific behavior there.
  • Maintain two contracts. Document both what the outside system expects and what your internal system promises.
  • Design for ambiguity. Use idempotency, lineage, quarantine, reconciliation, and durable identifiers so uncertain outcomes can be resolved.
  • Preserve meaning at the boundary. A short integration is not necessarily a simple integration. Measure quality by containment of assumptions, not by line count.

The Boundary Is Where Architecture Becomes Real

It is tempting to think of architecture as the large shapes: services, queues, databases, and deployment environments. Yet the character of a system is often decided in much smaller places, inside the method that maps an external status, the transform that handles a missing value, or the job that decides whether a repeated record is a duplicate.

Those decisions determine whether change remains local or spreads everywhere. They determine whether an outage becomes a recoverable event or a detective story. They determine whether a new provider can be added by writing one adapter or by rewriting the application’s assumptions.

The central lesson is not that every integration needs elaborate abstraction. It is that every integration already contains an abstraction, whether you design it or not. If you do not define the boundary, the external system will define it for you.

The best integration code therefore does something paradoxical: it makes the outside world easier to change by making its own responsibilities more explicit. A port is a promise about meaning. An adapter is a disciplined act of translation. A robust data job is not merely moving records. It is deciding what those records are allowed to mean when they enter a new world.

Once integration is viewed this way, “glue code” stops being the leftover material between important components. It becomes the place where a system proves that it knows what it believes.

Sources

← Back to Library

Hatch New Ideas with Glasp AI 🐣

Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)

Start Hatching 🐣