Skip to content

Infrastructure – The Technical Outside World

Infrastructure has a bad reputation.

As soon as the term comes up, many people think of ports, adapters, repository interfaces, factories, and multi-layered integration architectures. Diagrams full of arrows and even more boxes.

The introduction to this series describes Infrastructure as the first stop on the path from the technical outside world to the user. What actually happens at that first stop is far less spectacular than the name suggests.

For this article, a much simpler view is enough:

Infrastructure is where our application meets something it does not fully control.

A backend. A REST or GraphQL API. WebSocket or SSE. Local Storage. IndexedDB. A browser API. An authentication provider. An external SDK. Another bounded context.

“Not fully controlled” does not necessarily mean “owned by another company.” Even a backend team inside the same organization can change a contract without the frontend being able to prevent that change. What matters less is the organizational boundary and more who owns a structure and who decides how it evolves.

The concrete technology is secondary. Whether we use Angular HttpClient, fetch, Axios, or another client does not change the architectural responsibility.

Outside world
Infrastructure
our domain

Infrastructure accepts external technical structures and translates them into our own world in a controlled way.

That is all it needs to do at first.

The technical outside world is validated, normalized, and translated by the Infrastructure layer before data reaches the application's domain.

And that is exactly the point: Infrastructure is allowed to be boring.

A backend contract belongs to the backend first. An external SDK belongs to another vendor. A browser API contract belongs to the browser.

These models should not automatically become our own domain model just because they are the first structures to arrive in our application.

A small example is enough. A backend returns a customer:

type CustomerDto = {
id: string;
first_name: string;
last_name: string;
};

Internally, we work with:

type Customer = {
id: string;
firstName: string;
lastName: string;
};

This is not a sophisticated transformation. That simplicity matters because many real-world boundaries look exactly this unspectacular.

CustomerDto describes how the backend delivers a customer. Customer describes how our application talks about a customer. Both can evolve independently as long as a defined translation exists between them.

There is an obvious objection at this point:

function toCustomer(dto: CustomerDto): Customer {
return {
id: dto.id,
firstName: dto.first_name,
lastName: dto.last_name,
};
}

Why write additional code when the two structures barely differ?

Today, the mapper can indeed look almost unnecessary. Tomorrow, the backend may change its field names:

first_name → givenName
last_name → familyName

Then exactly this translation changes. Ideally, stores, business rules, facades, and components do not need to know anything about it.

A changed backend contract is absorbed by the mapper while the internal Customer model remains unchanged.

A five-line mapper is not overengineering if it prevents an external contract from becoming the application’s de facto domain model across fifty files.

The note on DTO leakage takes a closer look at what happens when DTOs cross this boundary unchecked and spread through state, application code, components, and templates.

For this article, one observation is enough: a small translation boundary can prevent the structure of an external system from gradually becoming the structure of our own application.

Another detail is easy to overlook in frontend development.

An HTTP response comes from an external source. After JSON parsing, we have JavaScript values: strings, numbers, objects, arrays, null.

A type assertion such as:

const customer = response as CustomerDto;

or a typed HTTP call such as:

http.get<CustomerDto>('/api/customers/42');

describes what the frontend expects. It does not prove that the backend actually returned that structure.

A TypeScript type does not validate external data. It describes at compile time which structure we intend to work with. Whether the data actually matches that structure at runtime may need to be checked separately at the system boundary.

That check belongs in Infrastructure.

The job of this boundary can be reduced to a few responsibilities:

unknown / external data
Infrastructure
├── validate
├── normalize
└── translate
our model

Typical responsibilities include:

  • runtime validation
  • checking required fields
  • normalizing external formats
  • converting date values
  • handling null and undefined in a controlled way
  • translating technical status codes
  • transforming DTOs into our own models
  • translating errors from external systems into our own technical errors

Libraries such as Zod can be used for runtime validation. Which concrete library performs that job is secondary to the architectural decision. What matters is where the validation happens.

Normalization should remain boring and unambiguous as well.

A known date format can be converted into a Date. An optional field can be normalized according to a defined technical rule. An external status code can be mapped to an internal technical or domain representation.

As soon as a discrepancy can no longer be interpreted unambiguously on a technical level, Infrastructure should not invent domain meaning to compensate for it.

Data that cannot be interpreted is a technical error first.

How the application presents that error to the user is already a different responsibility.

The Customer example uses HTTP because it makes the idea easy to see. The same boundary exists wherever data enters from the outside.

A value from localStorage is initially a string or null. It is not automatically the object that was stored there yesterday.

A payment provider’s SDK throws its own error classes with its own codes.

A WebSocket delivers messages whose structure may change with a new server version.

In all of these cases, the same question applies: do we pass the raw value into our application, or do we first translate it into something our feature understands?

Errors can be part of that translation as well:

try {
await paymentSdk.charge(order);
} catch (error) {
throw toPaymentInfrastructureError(error);
}

toPaymentInfrastructureError is allowed to know the SDK’s error codes and technical peculiarities. The rest of the application then works with its own controlled set of technical error cases.

If the payment provider changes, that foreign language stays at the Infrastructure boundary.

This is the most important boundary in this article.

Infrastructure may remove external uncertainty. It should not derive business rules from it.

An external contract might provide:

"ACTIVE"
CustomerStatus.Active

That translation can belong to Infrastructure.

Whether an active customer is allowed to place an order is a different question:

CustomerStatus.Active
May this customer place an order?

This is where domain logic begins.

Both operations can look similarly unremarkable in code. A switch, an if, perhaps a single assignment. Their responsibilities are still fundamentally different.

Infrastructure can translate things such as:

"2026-08-10T10:15:00Z" → Date
null → undefined
"C" → CreditHold
first_name → firstName

+State, on the other hand, answers questions such as:

May this order be cancelled?
May this customer place an order?
Is this state transition allowed?
Which actions are available according to the domain rules?

Infrastructure removes technical and structural uncertainty. Domain decisions begin after that.

What Infrastructure should – and shouldn’t – do

Section titled “What Infrastructure should – and shouldn’t – do”

Infrastructure should:

  • encapsulate external contracts
  • translate DTOs into our own models
  • normalize technical formats
  • validate external input
  • encapsulate technical APIs
  • translate foreign errors into our own technical errors
  • prevent external models from leaking uncontrolled into the application

Infrastructure should not:

  • implement business rules
  • make domain decisions
  • derive domain state
  • build ViewModels for concrete views
  • manage UI state
  • control navigation
  • open dialogs
  • take over Presentation responsibilities
  • collect logic simply because we already happen to be inside a technical service

The more interesting the business logic inside the Infrastructure layer becomes, the more carefully we should ask whether it really belongs there.

When the boundary becomes more substantial

Section titled “When the boundary becomes more substantial”

The trivial mapper from the Customer example is not yet a full Anti-Corruption Layer in the stricter sense described by Eric Evans in the context of Domain-Driven Design.

They do, however, share a defensive idea: an external model should not become our own model without control.

As long as first_name merely becomes firstName, the boundary remains deliberately unspectacular.

It becomes more interesting when different terms, status models, or domain meanings meet. Perhaps "ACTIVE" means something different in the external system than Active means inside our feature. Perhaps a legacy system exposes error codes whose meaning only exists there. Perhaps both systems model the same customer but understand that customer differently from a domain perspective.

At that point, mechanical field mapping eventually stops being enough.

The article on the Anti-Corruption Layer explores this deeper translation between an external model and our own model.

For the Infrastructure layer, the important point remains simpler: even a small explicit boundary can stop an external model from silently spreading through the entire application.

As much boundary as needed, as little Infrastructure as possible

Section titled “As much boundary as needed, as little Infrastructure as possible”

Not every primitive field needs its own Value Object. Not every API object automatically needs an interface, a repository, a port, an adapter, a factory, and several layers of abstraction.

Layering is not a competition to create as many files as possible.

A trivial contract does not necessarily have to be modeled twice either. What matters is the risk created by using it directly.

Useful questions include:

  • Who owns the contract?
  • Can it change independently of us?
  • How far would it spread into the application?
  • Is the semantics outside different from the semantics inside?
  • Do the data need to be normalized or validated?
  • How expensive would it be to decouple later?

A small technical lookup structure used only locally may not need another abstraction.

A payment status that later influences domain decisions deserves a deliberately placed boundary, even if its translation starts out as only a few lines of code.

The difference is not the number of lines.

It is the amount of coupling we create when we omit the boundary.

As much boundary as needed, as little Infrastructure as possible.

Infrastructure is allowed to stay boring.

At the end of the Infrastructure layer, we have received external data, validated it, normalized it, and translated it into a form our feature can trust.

No domain decision has been made yet.

"ACTIVE" has become CustomerStatus.Active. Whether that customer may place an order, whether an order may be cancelled, or which state transition is currently allowed is still undecided.

What happens to these data once they have crossed our technical boundary is the question where +State begins.

That is where the domain logic starts.