GO TO A SECTION

↑ ↓ to navigate · Enter to openExplore all sections →
FDEInterviews logo
Lesson 11 of 35 · FreeCourse syllabus
Course outline · choose a lesson

Translate source data without losing its meaning

Build an adapter that preserves identifiers, validates an explicit source contract and returns stable domain types. Test what can stay unchanged when a file becomes an API, and what requires a new business decision.

30 MIN

By FDEInterviews · Updated

WATCH: THIS LESSON10:51
Transcript

This is lesson ten of Foundations of Forward Deployed Engineering. Keep source-specific translation behind an adapter with an explicit contract. Preserve identity and meaning, reject unsupported values, and test the downstream behavior you expect to remain stable.

You can read an unfamiliar system and locate its boundaries. Now you'll write the boundary between a customer's order feed and a small reporting workflow, including the tests that reveal a destructive conversion.

A rename is easy. A changed meaning needs a decision. Consider a fictional freight integration.

A daily export uses three source field names, for the order number, the customer ID and the status. The report reads these names directly, a totals function converts quantities, and the export job has its own interpretation of status nine. Then a second region introduces zero nine. You now have several places to investigate, before deciding whether the records mean the same thing.

An adapter collects those source assumptions and produces types the rest of your application understands. If the source model and your model have different semantics, this translation boundary is often called an anti-corruption layer. It can live inside your application. It doesn't require a separate service. Microsoft's pattern guidance also discusses validation, consistency, and the cost of maintaining the layer.

The boundary does more than rename fields. A customer identifier is an identifier, even when it contains only digits. Removing its leading zeros can merge two customers. Converting the text to a number discards exactly the characters that told the two apart, and once they've collapsed, no downstream code can recover which record was meant. A timestamp without a timezone doesn't become UTC because that would be convenient. And the word closed may mean financially closed in one system, but physically delivered in another.

An adapter can't settle those questions on its own. Its job includes making the agreed answers visible.

Keep source assumptions together. The lesson's diagram shows a small integration. A larger one may have several adapters, for separate source contracts.

Tests, fixtures and transport clients will also mention source field names. Counting files is a useful search technique, but there's no rule that one changed file proves good architecture, and three prove bad architecture.

Use a more concrete review question. When a source representation changes without changing its meaning, can you update the translation and its tests, without rewriting the reporting calculation? And when meaning changes, which domain rules should change with it?

Write the contract before the conversion. This contract and its data are synthetic training material, not a customer's documented API. In a real engagement, record the source owner's confirmation and the contract version, and don't invent either.

The order number is nonempty text, and surrounding ASCII spaces are transport padding. The customer ID is nonempty text, preserved exactly, with no surrounding whitespace. The order date is a ten-character business date, year, month and day. Status one is open, nine and zero nine are closed, and X is cancelled. Line items are a nonempty list, each with a SKU and a positive integer quantity, written as ASCII digits. Extra fields are ignored by this report.

Each field also has something the adapter must avoid: removing interior characters or accepting an all-space order number, converting the customer ID to an integer or stripping its zeros, guessing a timezone or slicing an arbitrary timestamp, inferring that every zero-padded status code is equivalent, and accepting booleans, floats, negative quantities or blank SKUs. Nor should you claim that every additive change is safe for every consumer.

The workflow needs total item quantity, not line-level prices or inventory reservations. That's why the domain type contains a total. A fulfillment workflow would probably need the individual lines and their units, and this type would be insufficient.

The complete adapter uses only Python's standard library. Its status map lists every supported code, so zero nine maps to closed because someone wrote it down. Any other code gets no mapping, and the adapter raises a contract error, instead of guessing.

The customer ID stays text. It's never stripped of zeros, and never converted to a number. If it arrives with surrounding whitespace, that's a contract error, not something to trim.

Each quantity must be ASCII digits, and greater than zero. A boolean, a float or a negative number fails the check. Only then is it added to the total.

Run the adapter on its sample. Expect the order number, the customer ID with its three leading zeros, closed, and a quantity of 5. The zeros survive. The mapping for zero nine is explicit. And the date is validated as a date, rather than extracted from whatever text happened to arrive.

This is a teaching adapter for a bounded in-memory object. A deployed transport also needs response-size limits, authentication, pagination, deadlines and protected diagnostics. The raised errors identify fields, without copying customer values into logs. And the caller decides whether a rejected record can be held individually, or whether the whole report must wait for correction.

Now test the conversion that looks harmless. A second block runs beside the adapter. It builds a pair of orders that differ only in the customer ID.

The first keeps the sample's ID, with three leading zeros. The second uses the same digits without them. The adapter keeps them as two different customers. The deliberately destructive strip expression collapses them to one value. Both source IDs contain digits, but numeric-looking text doesn't establish numeric identity. And that's a correctness failure, even if every downstream sum still runs.

The block then feeds the adapter five invalid inputs, and expects a contract error each time: status zero zero nine, the impossible date February thirtieth, a full timestamp instead of a date, a customer ID with a leading space, and a quantity that's the boolean true. It prints: identity preserved, five invalid cases rejected.

Next, develop offline without taking data you can't keep. Use synthetic fixtures like the sample whenever they cover the behavior.

If an approved captured response is needed to reproduce a defect, establish where it may be stored, who may access it, how it's transformed, and when it must be deleted. Removing names alone may leave identifying IDs, free text or linked records, and permission to call an API doesn't automatically permit committing its responses to Git.

With an approved fixture, you can keep testing pure translation and reporting logic when the VPN is unavailable. This doesn't verify today's authentication, the freshness of the data, or pagination. Those need separate integration checks.

Finally, what changes when the nightly file becomes an API? It depends on which change it is, and each one needs its own evidence before you call it compatible.

If the order number field is renamed with identical identity rules, that's the source adapter, proven by the contract and paired mapping tests. A file that becomes a paginated API lands in transport and integration checks, needing complete enumeration, stable continuation behavior, limits and a retry policy. A date that becomes an instant changes the domain contract and translation, and needs the required business timezone and reporting cutoff.

If closed now includes cancelled orders, that belongs to the domain rules and reports, and it needs the business to agree what it means, and revised expected outcomes. And data that becomes available every minute reaches the workflow and operations, as well as the transport, needing a freshness measurement, load, access approval and ownership.

A seam contains representation changes. It can't make a different business meaning equivalent, or turn an unapproved route into an approved one. In the lesson's last diagram, the adapter is the one place allowed to know what the source meant.

Your turn. Pause the video here. Produce a one-page adapter contract and a passing script. Start with the contract table, then add one status and one quantity edge case, without weakening the existing checks. Change the fixture's status to zero zero nine. The current result must be rejection. If you decide it means closed, first write the new source-contract assumption, then add an explicit mapping and test. Then search an integration you know for raw status codes and field names, and separate expected occurrences in adapters, fixtures and tests from business calculations that depend on source representations. Describe one change you can isolate, and one semantic change that must reach the domain model.

To recap. Keep source representations behind an explicit translation contract, and preserve identifiers and business meaning, because normalization needs evidence. Rejected values need an operational policy outside the pure conversion function. Offline fixtures require appropriate handling, and can't establish live compatibility. And a transport upgrade may also change freshness, access, pagination or business semantics.

That's lesson ten. Next in the series, choose retries from outcome evidence and a time budget. The full lesson, with its exercise and drill, is on fdeinterviews.com.

TL;DR: Keep source-specific translation behind an adapter with an explicit contract. Preserve identity and meaning, reject unsupported values, and test the downstream behavior you expect to remain stable.

Where you are. You can read an unfamiliar system and locate its boundaries. Now you will write the boundary between a customer's order feed and a small reporting workflow, including the tests that reveal a destructive conversion.

A rename is easy; a changed meaning needs a decision

Take a fictional freight integration. A daily export uses ORDER_NBR, cust_id and sts; the report reads these names directly, a totals function converts quantities, and the export job has its own reading of status 9. Then a second region introduces 09, and you have several places to investigate before you can say whether the records mean the same thing.

An adapter collects those source assumptions and produces types the rest of your application understands. When the source model and your model have different semantics, this translation boundary is often called an anti-corruption layer, and it can live inside your application without needing a separate service. Microsoft's pattern guidance also discusses validation, consistency and the cost of maintaining the layer.

The boundary does more than rename fields. A customer identifier is still an identifier when it contains only digits, and removing its leading zeros can merge two customers. Converting the text to a number discards exactly the characters that told the two apart, and once they have collapsed no downstream code can recover which record was meant. A timestamp without a timezone does not become UTC because UTC would be convenient, and closed may mean financially closed in one system but physically delivered in another.

An adapter cannot settle those questions on its own, but part of its job is to make the agreed answers visible.

Source assumptions in one place

NO SEAM their payload report.py: reads ORDER_NBR totals.py: reads qty, casts export.py: checks sts == "9" Scattered source assumptions require scattered changes. WITH A SEAM their payload adapter Order report totals export Keep source mappings together. Test the meaning of each change. The adapter is also the only place that has to explain what "9" means.

The diagram shows a small integration; a larger one may have several adapters for separate source contracts, and tests, fixtures and transport clients will mention source field names too. Counting files is a useful search technique, but there is no rule that one changed file proves good architecture and three prove bad architecture.

Use a more concrete review question: when a source representation changes without changing its meaning, can you update the translation and its tests without rewriting the reporting calculation? When meaning changes, which domain rules should change with it?

The contract before the conversion

The following contract and data are synthetic training material, not a customer's documented API. In a real engagement, record the source owner's confirmation and the contract version; do not invent either.

FieldContract for this exerciseWhat the adapter must avoid
ORDER_NBRNonempty text; surrounding ASCII spaces are transport paddingRemoving interior characters or accepting an all-space ID
cust_idNonempty text, preserved exactly; no surrounding whitespaceConverting to an integer or stripping zeros
order_dtTen-character YYYY-MM-DD business dateGuessing a timezone or slicing an arbitrary timestamp
sts1 open, 9 and 09 closed, X cancelledInferring that every zero-padded code is equivalent
ln_itemsNonempty list; each item has a SKU and positive integer quantity encoded as ASCII digitsAccepting booleans, floats, negative quantities or blank SKUs
Extra fieldsIgnored by this reportClaiming every additive change is safe for every consumer

The workflow needs total item quantity, not line-level prices or inventory reservations, which is why the domain type below holds a total. A fulfillment workflow would probably need the individual lines and their units; this type would be insufficient.

Save this complete example as adapter.py. It uses only Python's standard library.

python
from dataclasses import dataclass
from datetime import date
from enum import Enum
import re

class ContractError(ValueError):
    pass

class OrderStatus(Enum):
    OPEN = "open"
    CLOSED = "closed"
    CANCELLED = "cancelled"

@dataclass(frozen=True)
class Order:
    number: str
    customer_id: str
    placed_on: date
    status: OrderStatus
    quantity: int

STATUS = {"1": OrderStatus.OPEN, "9": OrderStatus.CLOSED,
          "09": OrderStatus.CLOSED, "X": OrderStatus.CANCELLED}

def text_field(raw, key):
    value = raw.get(key)
    if not isinstance(value, str) or not value:
        raise ContractError(f"{key}: expected nonempty text")
    return value

def to_order(raw):
    if not isinstance(raw, dict):
        raise ContractError("order: expected object")
    number = text_field(raw, "ORDER_NBR").strip(" ")
    if not number or number != number.strip():
        raise ContractError("ORDER_NBR: invalid padding")
    customer = text_field(raw, "cust_id")
    if customer != customer.strip():
        raise ContractError("cust_id: surrounding whitespace")
    day = text_field(raw, "order_dt")
    if not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", day):
        raise ContractError("order_dt: expected business date")
    try:
        placed_on = date.fromisoformat(day)
    except ValueError as error:
        raise ContractError("order_dt: invalid date") from error
    status = STATUS.get(text_field(raw, "sts"))
    if status is None:
        raise ContractError("sts: unsupported status")
    lines = raw.get("ln_items")
    if not isinstance(lines, list) or not lines:
        raise ContractError("ln_items: expected nonempty list")
    total = 0
    for line in lines:
        if not isinstance(line, dict):
            raise ContractError("line: expected object")
        sku = text_field(line, "sku")
        if not sku.strip():
            raise ContractError("sku: blank")
        qty = text_field(line, "qty")
        if not re.fullmatch(r"[0-9]+", qty) or int(qty) <= 0:
            raise ContractError("qty: expected positive integer text")
        total += int(qty)
    return Order(number, customer, placed_on, status, total)

SAMPLE = {
    "ORDER_NBR": "  A-4471 ", "cust_id": "00099421",
    "order_dt": "2026-03-14", "sts": "09",
    "ln_items": [{"sku": "X1", "qty": "2"},
                 {"sku": "X2", "qty": "3"}],
}

if __name__ == "__main__":
    order = to_order(SAMPLE)
    print(order.number, order.customer_id, order.status.value, order.quantity)

Run python adapter.py and expect A-4471 00099421 closed 5. The zeros survive, the mapping for 09 is explicit, and the date is validated as a date rather than sliced out of whatever text happened to arrive.

This is a teaching adapter for a bounded in-memory object, and a deployed transport also needs response-size limits, authentication, pagination, deadlines and protected diagnostics. The raised errors name fields without copying customer values into logs. The caller decides whether a rejected record can be held individually or whether the whole report must wait for correction.

The conversion that looks harmless

Run this second block beside adapter.py:

python
from copy import deepcopy
from adapter import SAMPLE, ContractError, to_order

first = deepcopy(SAMPLE)
second = deepcopy(SAMPLE)
second["cust_id"] = "99421"
assert to_order(first).customer_id != to_order(second).customer_id
assert len({first["cust_id"].lstrip("0"),
            second["cust_id"].lstrip("0")}) == 1

for field, bad in [("sts", "009"), ("order_dt", "2026-02-30"),
                   ("order_dt", "2026-03-14T00:00:00"),
                   ("cust_id", " 00099421")]:
    raw = deepcopy(SAMPLE)
    raw[field] = bad
    try:
        to_order(raw)
    except ContractError:
        pass
    else:
        raise AssertionError(f"accepted invalid {field}")

raw = deepcopy(SAMPLE)
raw["ln_items"][0]["qty"] = True
try:
    to_order(raw)
except ContractError:
    pass
else:
    raise AssertionError("accepted a boolean quantity")
print("identity preserved; five invalid cases rejected")

The first pair is the useful surprise: both source IDs contain digits, but numeric-looking text does not establish numeric identity. The deliberately destructive lstrip expression collapses them to one value, which is a correctness failure even though every downstream sum still runs.

Working offline without keeping data you may not keep

Use synthetic fixtures like SAMPLE whenever they cover the behavior. If an approved captured response is needed to reproduce a defect, establish where it may be stored, who may access it, how it is transformed and when it must be deleted. Removing names alone may leave identifying IDs, free text or linked records behind, and permission to call an API does not automatically permit committing its responses to Git.

With an approved fixture, you can keep testing pure translation and reporting logic when the VPN is down. That says nothing about today's authentication, data freshness or pagination, which need separate integration checks.

What changes when the nightly file becomes an API?

ChangeLikely locationEvidence needed before calling it compatible
ORDER_NBR becomes orderNumber with identical identity rulesSource adapterContract and paired mapping tests
File becomes paginated APITransport and integration checksComplete enumeration, stable continuation behavior, limits and retry policy
Date becomes an instantDomain contract and translationRequired business timezone and reporting cutoff
closed now includes cancelled ordersDomain rules and reportsAgreed business meaning and revised expected outcomes
Data becomes available every minuteWorkflow and operations as well as transportFreshness measurement, load, access approval and ownership

A seam contains representation changes. It cannot make a different business meaning equivalent, or turn an unapproved route into an approved one.

One place that knows what the source meant 1 Source fields arrive their names, their codes 2 One adapter holds them the only place that knows 3 Write the contract first and whose confirmation it is 4 Convert to a domain type shaped by the workflow 5 Reject what is not covered do not guess a default 6 Test the harmless ones they are the expensive ones 7 The rest stays clean no source names downstream A daily export uses one set of names, a second region sends a status of 09 where the first sent 9, and the report, the totals function and the export job each hold their own interpretation. The adapter is where that stops spreading. Record the source owner's confirmation and the contract version. Do not invent either, and do not let a fixture you wrote become the evidence that the source behaves that way. A seam changes representation. It cannot make a different business meaning equivalent, and it cannot turn an unapproved route into an approved one. A status of 009 must be rejected under the current contract. If you decide it means closed, first write the new source assumption, then add the explicit mapping and its test, in that order.

The spine above is what a record does as it crosses the seam, with the adapter marked as the one place allowed to know what the source meant.

Do this before moving on

Produce a one-page adapter contract and a passing script. Start with the table above, then add one status edge case and one quantity edge case without weakening the existing checks. Change the fixture's status to 009, and the current result must be a rejection. If you decide it means closed, first write the new source-contract assumption, then add an explicit mapping and test.

Search an integration you know for raw status codes and field names. Separate expected occurrences in adapters, fixtures and tests from business calculations that depend on source representations. Describe one change you can isolate and one semantic change that must reach the domain model. A useful handover tells the next engineer both.

Go deeper

Key takeaways

  • Keep source representations behind an explicit translation contract.
  • Preserve identifiers and business meaning; normalization needs evidence.
  • Rejected values need an operational policy outside the pure conversion function.
  • Offline fixtures require appropriate handling and cannot establish live compatibility.
  • A transport upgrade may also change freshness, access, pagination or business semantics.

Check yourself

Answer before you look. Recalling it is what makes it stick; recognising it does not.

  1. 1The source contains customer IDs 00099421 and 99421. What justifies stripping zeros?

  2. 2The API replaces a date with a timezone-free timestamp. Is taking the first ten characters sufficient?

  3. 3Why can a nightly-file-to-API change require work beyond one adapter?

Sign in free to save your progress and return to this lesson.

Back to the course syllabus