When Infrastructure Starts Leaking Into Application Code

1. The Boundary Is Not Only About Project References

Infrastructure leakage is often discussed as a dependency problem.

An application project references an infrastructure project. A domain class depends on a framework type. A service imports a provider SDK. A workflow reaches directly into database-specific behavior. From that point of view, the boundary seems easy to check: look at the project references and see whether the dependencies point in the right direction.

That matters.

Project references are one of the most visible signs of architecture. If the application layer depends directly on a concrete SMTP implementation, a database context, a storage SDK, or an HTTP provider client, the boundary is already weakened. The application is no longer speaking only in terms of the work it wants done. It is starting to know how that work is performed.

But infrastructure leakage is not only about references.

A project can have the “right” dependency direction and still leak infrastructure concepts into application code. The application may depend on an abstraction, but the abstraction may still be shaped around infrastructure details. It may return provider status codes, expose database query concepts, require framework-specific options, or force callers to understand technical failure modes that should have been translated at the edge.

The boundary can look clean from the outside while still being noisy from the inside.

For example, an application service may depend on an IEmailSender rather than an SMTP class. That is a good start. But if the application service must know about SMTP authentication failures, mail client launch behavior, provider-specific response codes, or retry rules tied to a particular transport, the abstraction has not fully protected the workflow. The dependency has been inverted, but the language has still leaked.

That is the more subtle form of infrastructure leakage.

It happens when application code starts making decisions using infrastructure vocabulary instead of application vocabulary. The code may no longer reference the concrete provider, but it still thinks in terms of provider behavior. It may no longer reference the database context, but it still reasons in terms of persistence mechanics. It may no longer depend on a framework class directly, but it still shapes its workflow around framework concerns.

A boundary should do more than hide a concrete type.1

It should protect the application from decisions that do not belong there.

The application layer should usually be able to say what it needs in terms of the use case: load this article, save this change, send this message, validate this request, publish this result. Infrastructure should decide how those actions are performed. It can know about SQL, SMTP, HTTP, files, queues, provider payloads, and external error codes. The application should receive the result in language it can use for workflow decisions.

That is why interfaces alone are not enough.

An interface can be placed in the right project and still expose the wrong concepts. A method can be abstract and still be too technical. A result can be provider-independent in name while still forcing the caller to handle provider-specific meaning.

Good boundaries are not only structural. They are linguistic and behavioral.

They shape what each part of the system is allowed to know and decide. They keep infrastructure mechanisms at the edge and let application code stay focused on workflow, intent, and outcome.

When infrastructure starts leaking into application code, the first sign may not be a forbidden project reference. It may be a method signature, a result type, an exception path, or a conditional branch that makes the application speak a language it should not need to know.

2. What Infrastructure Leakage Looks Like

Infrastructure leakage is not always obvious.

Sometimes it looks like a direct dependency: application code imports a provider SDK, uses a database context directly, or creates an HTTP client inside a workflow. Those cases are easy to recognize because the technical mechanism is visible in the code.

But leakage can also be more subtle.

It can appear in names, method signatures, result types, exceptions, configuration values, and conditional logic. The code may not directly reference the infrastructure implementation, but the application layer may still be forced to understand infrastructure concepts in order to do its work.

For example, an application service may call a repository method named around a database concern rather than an application need. It may pass query flags that only make sense because of how the data is stored. It may inspect a persistence-specific error to decide whether a workflow should continue. The repository is still behind an interface, but the application is now reasoning in terms of persistence mechanics.

The same can happen with external providers.

A workflow may call an abstraction such as IEmailSender, but then branch on provider-specific failure categories. It may handle SMTP authentication differently from an API rate limit, not because the workflow has a meaningful response to each case, but because those technical details escaped through the boundary.

At that point, the abstraction is only partial.

The application no longer depends on the concrete sender, but it still depends on the sender’s internal world.

Infrastructure leakage often shows up as application code asking questions it should not need to ask:

  • Did SQL return a duplicate key error?
  • Did the provider return HTTP 429?
  • Was this an SMTP authentication failure?
  • Did the storage SDK throw this specific exception?
  • Which EF Core tracking mode should this workflow use?

Those questions may be valid somewhere. They may be important inside an infrastructure adapter, a repository implementation, or a provider-specific integration. But if ordinary application workflow code needs to ask them, the boundary may be leaking.

Application code should usually ask questions closer to the use case:

  • Was the request valid?
  • Was the item found?
  • Was there a conflict?
  • Was delivery attempted?
  • Did delivery fail?
  • Can the workflow continue?

Those questions belong to the application’s vocabulary. They describe outcomes the workflow can act on without knowing the technical cause in detail.

Another sign of leakage is when a change in infrastructure forces unrelated application code to change.

If replacing one mail provider with another requires many application services to handle new provider error codes, the provider boundary is too thin. If changing the database query strategy changes how callers express workflow intent, persistence details are too visible. If moving from local storage to cloud storage changes business decisions, storage mechanics have escaped their proper place.

Leakage can also be found in tests.

A test for application behavior should not usually need to set up database-specific options, provider-specific status codes, or framework-specific objects. If it does, the test may be revealing that the application behavior is too entangled with infrastructure concerns.

This does not mean application code should be ignorant of all technical failure.

The application may need to know that delivery failed, that persistence could not complete, or that an external dependency is unavailable. But those outcomes should be expressed in application-level terms. The infrastructure layer can preserve technical details for diagnostics, logging, support, or retries, while the application receives the meaning it needs for the workflow.

Infrastructure leakage is therefore not only about where code lives.

It is about what language the code is forced to speak.

When application code starts speaking SQL, SMTP, HTTP provider, filesystem, or framework language, it is worth asking whether the infrastructure boundary is still doing its job.

3. Why Leaks Often Start as Convenience

Infrastructure leakage rarely starts as a deliberate architectural decision.

It usually starts as convenience.

A workflow needs one extra piece of information from the database, so a repository method exposes a persistence-specific option. A provider returns a useful status code, so the application service checks it directly. A storage SDK throws a specific exception, so the caller catches it because that is the fastest way to distinguish one outcome from another. A configuration setting already exists, so a little workflow behavior is added around it.

The change is small. The reason is practical. The code works.

That is what makes the leak easy to accept.

In the moment, it may feel excessive to create a better abstraction, introduce a result type, move mapping logic into an adapter, or clarify the boundary. The direct solution is visible and quick. The infrastructure detail is already available, and using it immediately solves the problem in front of the developer.

That is not laziness. It is often a reasonable local response to pressure.

The problem is that convenience has a way of becoming contract.

Once the application service checks a provider status code, that status code is no longer just a provider detail. It has become part of how the workflow behaves. Once a repository exposes a query flag shaped by persistence mechanics, callers may begin to depend on that flag. Once a configuration value controls workflow behavior, that value may become a hidden policy switch.

A small shortcut becomes something future code must understand.

This is especially likely when the first leak appears harmless. One if statement around an HTTP status code does not look like architectural damage. One catch block for a database exception does not look like a major boundary problem. One provider-specific enum in a result type may feel like useful information rather than coupling.

But those small details teach the codebase a pattern.

They show future changes where it is acceptable to put similar decisions. The next developer may follow the example, not because it is ideal, but because it is already there. Over time, the application layer starts collecting infrastructure knowledge in many small places.

The system still has abstractions, but the abstractions no longer fully protect it.

This kind of leakage is also hard to remove later because it becomes mixed with legitimate workflow logic. The application service may be partly making application decisions and partly interpreting infrastructure details. Separating those concerns later requires understanding which parts express the use case and which parts only exist because of the current provider or storage mechanism.

That is why convenience leaks often become expensive.

The first change saves time. The later cleanup costs more because the leaked detail has started to influence design, tests, and caller expectations.

A useful test is to ask whether the application code would still make sense if the infrastructure mechanism changed.

If SMTP became an API provider, would the workflow still read naturally? If SQL storage became a different persistence mechanism, would the application service still express the same intent? If a provider changed its status codes, would the application contract remain stable?

If the answer is no, the convenient detail may have crossed the boundary.

That does not mean every shortcut is equally dangerous. Real systems contain tradeoffs, and sometimes a small leak is accepted deliberately because the cost of avoiding it is too high. But it should be recognized as a tradeoff, not mistaken for a clean design.

Convenience is not the problem by itself.

The problem is when convenience quietly decides the shape of the architecture.2

4. When Application Code Starts Speaking Infrastructure Language

One of the clearest signs of infrastructure leakage is a change in language.

Application code should usually speak in terms of the workflow. It should talk about requests, outcomes, validation, conflicts, delivery, publishing, approval, cancellation, and the next step in the use case. Those words help the code explain what the application is trying to accomplish.

Infrastructure has a different language.

It talks about SQL queries, HTTP status codes, SMTP responses, file paths, storage containers, provider payloads, tracking modes, connection strings, serialization formats, and framework-specific options. Those words are necessary somewhere. They describe how the system interacts with the outside world.

The problem starts when the application layer has to use that language to make its normal decisions.

For example, an application service may not directly depend on a concrete provider, but it may still branch on an HTTP status code. A workflow may not reference the database context, but it may still pass flags that only make sense because of how the repository uses the database. A result type may look generic, but still expose values that are meaningful only for a specific provider.

The application code is no longer expressing the use case clearly.

It is translating infrastructure details while also trying to coordinate the workflow.

That mixed language makes the code harder to read. A developer should be able to understand an application service primarily as a description of the use case. When the method is filled with provider codes, persistence options, framework exceptions, and transport-specific conditions, the workflow becomes harder to see.

The code may still be technically correct, but its intent becomes noisy.

This also creates a dependency that is not always visible in the project graph. The application may depend on an interface, not a concrete implementation. But if the interface exposes infrastructure language, the application still depends on the infrastructure concept. The dependency has moved from the reference graph into the contract.

That kind of dependency is easy to miss.

A method named SendAsync may look fine until the result exposes SMTP-specific outcomes. A repository method may look abstract until its parameters are shaped around database loading behavior. A storage abstraction may look provider-neutral until callers have to know which SDK exception means the file was missing.

The names reveal the leak.

Application code should not normally need to know whether a provider returned 429, whether SQL produced a duplicate key violation, whether an entity was tracked, or whether a storage SDK used one exception type instead of another. Those facts may matter inside infrastructure. But the application usually needs the meaning, not the mechanism.

That meaning should be translated before it reaches the workflow.

429 may become TemporarilyUnavailable. A duplicate key violation may become Conflict. A missing file may become NotFound. An SMTP failure may become DeliveryFailed. The exact translation depends on the application, but the principle is the same: infrastructure speaks technical language at the edge; application code receives application-level meaning.

This is not about hiding useful information.

Technical details may still be logged, preserved for diagnostics, attached to a failure result, or made available to support tools. The point is that ordinary workflow code should not need those details to decide what happens next.

When application code starts speaking infrastructure language, the boundary has become too thin. The application is no longer protected from how the work is performed. It is being asked to understand the outside world in order to express its own use case.

A good boundary lets application code stay fluent in the language of the application.

5. Provider Details Should Not Become Workflow Decisions

External providers often return detailed information.

A mail provider may distinguish between authentication failure, invalid recipient, rate limiting, temporary outage, rejected content, and delivery timeout. A payment provider may return different codes for expired cards, insufficient funds, suspected fraud, duplicate requests, or configuration problems. A storage provider may distinguish between missing files, permission errors, quota limits, and unavailable regions.

Those details matter.

They can be useful for diagnostics, logging, support, retries, and provider-specific handling. They may help explain why something failed and what should be investigated. Inside the infrastructure adapter, they are part of the reality that has to be handled.

But provider details should not automatically become workflow decisions.

The application workflow usually needs a smaller and more stable vocabulary. It may need to know that delivery failed, that a payment was rejected, that a dependency is temporarily unavailable, that a conflict occurred, or that an operation could not be completed. It does not always need to know the exact provider code that caused the outcome.

The provider may have twenty ways to describe failure.

The workflow may only have three meaningful responses.

That difference matters because workflows should be designed around application decisions, not provider taxonomies.

If an application service starts branching on provider-specific details, the provider model begins to shape the use case. A mail workflow may start behaving differently for every SMTP or API error, even when the user experience and application response are the same. A payment workflow may become tied to one provider’s error categories. A storage workflow may begin to assume that missing data, permission problems, and unavailable regions should be interpreted according to one SDK’s exception model.

At that point, changing the provider becomes harder than it should be.

The code may not depend directly on the provider SDK, but it depends on the provider’s way of thinking. Replacing the provider then requires more than changing infrastructure. It requires changing application logic, tests, result handling, and perhaps even user-facing behavior.

That is a sign that the provider details have crossed the boundary.

A better design translates provider outcomes before they reach the workflow.

Provider-specific failures
    -> Authentication error
    -> Rate limit
    -> Timeout
    -> Rejected payload

Application-level outcomes
    -> DeliveryFailed
    -> TemporarilyUnavailable
    -> InvalidRequest

The exact mapping depends on the system, but the direction is important. Infrastructure reads the provider’s language. Application code receives the application’s language.

This does not mean all provider detail is thrown away.

A result may still carry a diagnostic message, a provider error code, a correlation id, or an exception that can be logged. Support tools may need that information. Operations may need it. Developers may need it when investigating failures.

But those details should support the workflow result, not define it.

For example, a sender may map several provider-specific failures to DeliveryFailed, while preserving the original provider code for diagnostics. The application service can then decide how the workflow should respond to failed delivery without knowing whether the provider reported an SMTP authentication problem, an HTTP timeout, or a rejected payload.

That keeps the application code stable.

It also makes tests clearer. Application tests can assert that a failed provider call produces the expected application-level outcome. Provider adapter tests can verify the detailed mapping from provider behavior to that outcome. Each test stays close to the decision it is trying to protect.

The provider boundary should therefore act as a translator.

It should not pretend provider details do not exist. It should not flatten every failure into a vague error. But it should prevent provider-specific language from becoming the normal decision language of the application.

Provider details belong at the edge.

Workflow decisions belong in the application.

6. Repositories Should Hide Persistence Mechanics, Not Business Meaning

Repositories are often introduced to protect the application from persistence details.

That is a useful goal.

The application should usually not need to know how a query is written, which tables are involved, which relationships need to be included, whether data comes from SQL Server, SQLite, a document store, or another storage mechanism. It should ask for the data it needs in terms of the application or domain model, and the repository should decide how to retrieve or persist it.

That is the part repositories are good at hiding.

They can hide query syntax, database connections, transactions, tracking behavior, mapping, eager loading, indexes, and other persistence concerns. Those details matter, but they are not usually the language of the application workflow.

The danger appears when the repository hides more than mechanics.

A repository can begin to hide business meaning.

For example, a method named GetAvailableArticlesAsync may look harmless, but the important question is what “available” means. If it means “records that can be loaded efficiently with the required relationships,” that may be a persistence concern. If it means “articles the current workflow is allowed to publish,” then the repository may be owning a business or workflow decision.

The same problem appears with filtering.

Some filters are persistence mechanics. A repository may filter out soft-deleted rows because the application treats them as absent everywhere. It may include only records belonging to a tenant because the storage boundary is tenant-scoped. It may apply technical constraints needed to protect the integrity of the data access pattern.

Other filters are business decisions.

A query that returns only approved articles, only visible updates, only licenses eligible for renewal, or only items the current user may act on may be doing more than retrieving data. It may be deciding what the workflow means. If that decision is not visible at the application or domain level, the repository has become more than a persistence boundary.

That can make the system harder to reason about.

The caller asks for data and receives a result, but part of the decision has already been made invisibly. A workflow may appear to allow an operation, but the repository silently excludes the record. Another workflow may use a different repository method and get a different interpretation of the same rule. Tests may need to know which repository method hides which business decision.

The persistence boundary has become a policy boundary.

That does not mean repositories should be completely generic or empty. A repository should still provide useful methods that match application needs. A method such as GetArticleForEditingAsync may be better than exposing a general query interface everywhere. The problem is not that repository methods have intent. The problem is when the repository becomes the only place where business meaning is defined.

A good repository method should make persistence easier without hiding ownership of the rule.

If the application decides that only approved articles can be scheduled, the repository may provide the data needed to make that decision. It may even offer a query optimized for that use case. But the rule itself should be visible where the workflow or domain meaning is expressed.

This distinction is often subtle.

A repository method can be named around the use case without owning the use case. It can retrieve “article with scheduling data” without deciding whether scheduling is allowed. It can retrieve “licenses near expiry” if that is a query shape, while the application or domain decides what renewal means. It can retrieve “pending messages” while the workflow decides which ones should be sent now.

The repository hides how the data is found.

It should be careful about hiding why the data matters.

When repositories own too much meaning, changing persistence can also become harder. Business rules become embedded in queries, query names, includes, filters, and projections. A database optimization may accidentally change workflow behavior. A new storage mechanism may require rediscovering rules that were never explicit outside persistence code.

That is infrastructure leakage in a quiet form.

The application may not reference the database directly, but the database-facing code is still deciding application behavior.

Repositories are most useful when they protect application code from persistence mechanics while keeping business meaning visible.3 They should make data access clear, reliable, and testable. But they should not become the hidden place where the system decides what the workflow means.

7. Configuration Can Leak Infrastructure Choices Upward

Configuration is often treated as harmless because it sits outside the code.

A connection string changes. A provider name is selected. A timeout is adjusted. A feature flag is enabled. A storage path is moved. A mail sender is switched from one implementation to another.

Those are useful forms of configuration.

They allow the same application to run in different environments without changing the code. They let infrastructure choices vary between development, testing, staging, and production. They make operational adjustments possible without rebuilding the system.

But configuration can also become a path for infrastructure leakage.

The problem starts when configuration values begin to shape application logic directly. Instead of selecting an infrastructure implementation behind a boundary, the configuration value is read by application code and used to decide workflow behavior. Instead of providing a technical value to infrastructure, it becomes a hidden branch in the use case.

For example, the application layer may check which mail provider is configured and then handle the workflow differently for SMTP, mailto, or an API-based sender. It may read a storage mode and then decide which business outcome a missing file should produce. It may inspect a database-related setting and use that to decide which records are valid for a workflow.

The configuration value may look simple.

But the application is now aware of infrastructure choices.

That makes the boundary weaker. The workflow no longer depends only on application-level concepts. It also depends on which technical mechanism happens to be configured. Changing the configured provider may change application behavior in ways that are not obvious from the workflow itself.

This can be especially hard to notice because configuration often feels external to architecture.

But configuration is part of architecture when it changes how the system behaves.

A setting such as SmtpHost is clearly infrastructure. It belongs close to the sender implementation. A setting such as StorageRootPath is also technical. It belongs near the storage adapter. But a setting such as UseProviderSpecificRetryHandling or TreatMissingBlobAsDraft is no longer only technical. It starts to express policy or workflow meaning.

That kind of setting deserves attention.

Sometimes the right answer is to keep the configuration, but move the decision. Configuration may select the implementation. The implementation may translate its technical behavior into an application-level result. The application workflow can then react to that result without knowing which provider or storage mechanism produced it.

For example:

Configuration
    -> Selects SMTP sender

SMTP sender
    -> Handles SMTP details
    -> Maps technical failures to delivery outcomes

Application workflow
    -> Handles DeliveryFailed

That keeps the variation where it belongs.

The application does not need to know that SMTP is configured. It only needs to know whether the configured sender could perform the work. The sender knows the provider-specific details. Configuration selects the sender. The workflow receives the application-level outcome.

This separation also makes tests cleaner.

Application tests can use a fake or recording implementation without reproducing production configuration. Infrastructure tests can verify that the configured provider behaves correctly. Configuration tests can verify that the correct implementation is wired. Each test stays close to the responsibility it protects.

When configuration leaks upward, tests often become more complicated. Application tests have to set infrastructure options. Workflow tests have to know provider names. A missing configuration value may break behavior that should not depend on configuration at that level.

That is a sign that configuration is carrying too much meaning.

Good configuration should make the system adaptable without making the application layer aware of every technical choice. It should supply values and select mechanisms, but it should not become the place where workflow meaning hides.

Configuration can shape infrastructure.

It should be careful about shaping the application’s language.

8. Exceptions, Status Codes, and Technical Results Need Translation

Infrastructure often reports failure in technical terms.

A database may throw an exception. An HTTP API may return a status code. A mail provider may return a provider-specific error response. A filesystem operation may fail with an access error. A queue may reject a message. A storage service may return a code that only makes sense in the context of that provider.

Those signals are important.

They tell the infrastructure code what happened at the technical boundary. They may contain information that is useful for diagnostics, logging, retries, support, or provider-specific handling. Ignoring them completely would make the system less observable and harder to troubleshoot.

But they are not always the right language for application code.

A workflow usually does not need to know that an HTTP provider returned 429, that SQL reported a duplicate key violation, or that a storage SDK threw a particular exception type. The workflow needs to know what the technical outcome means for the use case.

That meaning requires translation.

Technical signal
    -> HTTP 429
    -> SQL duplicate key
    -> SMTP authentication failure
    -> Storage object missing

Application-level outcome
    -> TemporarilyUnavailable
    -> Conflict
    -> DeliveryFailed
    -> NotFound

The exact mapping depends on the application. An HTTP 404 may mean NotFound in one workflow and UnexpectedFailure in another. A duplicate key violation may mean Conflict, or it may reveal a programming error if the application should have prevented the duplicate earlier. A timeout may mean TemporarilyUnavailable, DeliveryFailed, or a retriable infrastructure problem depending on the operation.

That is why translation should be deliberate.

It should not be left to every caller to interpret raw technical signals independently. If each application service decides for itself what a provider status code means, the system will likely become inconsistent. One workflow may treat a timeout as retriable, another as unexpected, and another as a generic failure. The technical signal is the same, but the application meaning becomes scattered.

A boundary adapter is a better place for that translation.

The adapter can understand the provider or framework details, preserve diagnostic information, and map the technical outcome into a stable application-level result. The application service can then respond to the result without knowing the exact provider behavior that produced it.

For example, a mail adapter may catch several provider-specific exceptions and return DeliveryFailed. A repository may translate a duplicate key violation into Conflict if that is the agreed application meaning. A storage adapter may translate a missing object into NotFound while preserving the provider error code for logs.

This keeps workflow code simpler.

The application service can ask: was the request valid, did the operation succeed, was there a conflict, was the dependency unavailable, did delivery fail? It does not need to inspect exception messages, parse error payloads, or compare numeric status codes unless those details are genuinely part of the application contract.

The technical details do not disappear.

They can still be logged, attached as diagnostics, included in telemetry, or preserved for support investigations. Translation is not the same as throwing information away. It is deciding which information belongs to the workflow and which belongs to diagnostics.

That distinction matters because raw technical signals are often unstable.

Provider codes can change. Exception messages can change. SDK exception types can change. Database mechanisms can change. If application logic depends directly on those details, infrastructure changes become application changes.

Stable application-level outcomes give the system more room to evolve.4

The infrastructure can adapt to new technical signals while the application continues to handle the same meaningful results. That is one of the main reasons boundaries exist.

Exceptions, status codes, and technical result objects are not bad. They are simply too low-level to be the normal decision language of application code. They need to be translated into outcomes the application can reason about deliberately.

Diagram showing infrastructure-specific signals being translated at the boundary into application-level outcomes.

Infrastructure details should be translated at the boundary so application code can work with stable workflow outcomes instead of provider-specific signals.

9. How Infrastructure Leakage Makes Tests Heavier

Tests often reveal infrastructure leakage before the architecture diagram does.

A workflow may look clean when reading the production code, but the test tells a different story. To test one application decision, the test may have to configure a database provider, prepare framework-specific options, simulate an HTTP status code, construct a provider exception, or reproduce part of the infrastructure environment.

That friction is a signal.

It may mean the application code is too dependent on infrastructure language.

If a test for workflow behavior has to know about SQL errors, SMTP authentication failures, storage SDK exceptions, or HTTP provider payloads, the workflow is probably receiving too much technical detail. The test is not only arranging the application scenario. It is also arranging the mechanics of the outside world.

That makes tests heavier than they need to be.

A test for application behavior should usually be able to speak in application terms. It should arrange that delivery fails, that a record is not found, that a conflict occurs, or that a dependency is unavailable. It should not always need to know exactly how a particular provider reports those situations.

When infrastructure details leak upward, test setup becomes more specific.

Instead of using a simple fake sender that returns DeliveryFailed, the test may need to create an SMTP exception. Instead of arranging a repository result that represents NotFound, it may need to reproduce a database behavior. Instead of verifying that a workflow handles a conflict, it may need to trigger the exact technical condition that one persistence provider uses to report a duplicate.

That kind of test can still be valuable at the infrastructure boundary.

But it is usually too detailed for an application workflow test.

The distinction matters because different tests should protect different decisions. An infrastructure test can verify that an SMTP authentication failure is translated into DeliveryFailed. An application test can verify that the workflow responds correctly when delivery fails. Those are related behaviors, but they are not the same decision.

If they are tested together everywhere, the test suite becomes more fragile.

A provider change may break many application tests even though the workflow meaning has not changed. A persistence optimization may require rewriting tests that were supposed to describe business behavior. A framework upgrade may affect tests that should not have known about the framework at all.

That is one of the costs of leakage.

It pulls technical change into tests that should have been protected by application-level contracts.

Good boundaries make tests lighter because they allow each test to arrange the decision at the right level. Application tests can use simple fakes, stubs, recording implementations, or in-memory collaborators that speak the same language as the application. Infrastructure tests can focus on provider behavior, mapping, serialization, queries, and external mechanics.

The result is not that infrastructure disappears from testing.

It is tested where it belongs.

For example, a mail adapter can have tests for provider-specific error translation. A repository can have tests for important query behavior. A storage adapter can have tests for mapping missing files, access errors, or provider failures. But an application service test should not need to carry all of that detail unless the workflow itself depends on it.

This separation also makes negative-path testing easier.

If application code receives stable outcomes such as NotFound, Conflict, DeliveryFailed, or TemporarilyUnavailable, tests can cover those paths directly. They do not need to reverse-engineer the infrastructure condition that happens to produce each one.

That makes the test more readable.

It also makes the test more durable. The provider may change. The database may change. The SDK may change. The application contract can remain the same, and the application tests can continue to describe the same behavior.

When tests become heavy, slow, and full of infrastructure setup, it is worth asking whether they are testing the right decision at the right level.

Sometimes the answer is that an integration test is needed. But often the answer is that infrastructure has leaked into application code, and the test is exposing the leak.

10. Refactoring Toward Application-Level Contracts

When infrastructure has leaked into application code, the goal is not only to move code around.

The goal is to restore the application-level contract.

That distinction matters. It is possible to move a provider call behind an interface and still leave the application thinking in provider terms. It is possible to hide a database context and still expose persistence-shaped query options. It is possible to wrap an HTTP client and still require callers to handle raw status codes.

The structure may improve, but the leak may remain.

A useful refactoring starts by asking what the application actually needs to know.

Does the workflow need to know that SMTP authentication failed, or does it only need to know that delivery failed? Does it need to know that SQL reported a duplicate key violation, or does it need to know that there was a conflict? Does it need to know that a provider returned 429, or does it need to know that the dependency is temporarily unavailable?

Those questions help separate technical cause from application meaning.

Once the application-level meaning is clear, the boundary can be reshaped around that meaning. Instead of returning provider-specific results, an infrastructure adapter can return stable outcomes. Instead of exposing database mechanics, a repository can offer methods that reflect the data the workflow needs. Instead of letting application services parse exceptions, the adapter can translate exceptions into results the workflow understands.

For example, a sender abstraction should probably not force callers to understand every provider-specific failure. It might return an application-level result such as Sent, ValidationFailed, DeliveryFailed, or TemporarilyUnavailable, while preserving provider diagnostics separately for logging or support.

The application service can then make workflow decisions using stable language.

That is the contract the application should depend on.

Refactoring toward such a contract often happens in small steps.

First, identify where the application code is speaking infrastructure language. Look for provider codes, database-specific exception handling, framework types, SDK result objects, technical flags, and conditionals based on configured mechanisms.

Second, decide which application-level outcome each technical detail represents. This is not always a one-to-one mapping. Several provider errors may become DeliveryFailed. A duplicate key may become Conflict in one workflow and UnexpectedFailure in another. A timeout may become TemporarilyUnavailable, DeliveryFailed, or a retriable failure depending on the use case.

Third, move the translation closer to the infrastructure boundary. The adapter, repository, or provider implementation should understand the technical signal and map it into the application vocabulary.

Fourth, update the application code to depend on the translated outcome rather than the technical detail.

After that, the tests usually become simpler.

Application tests can arrange application-level outcomes directly. Infrastructure tests can verify the mapping from technical behavior to those outcomes. The workflow no longer needs to reproduce provider-specific failures just to prove that it handles delivery failure, conflict, or missing data correctly.

This kind of refactoring also makes future infrastructure changes less invasive.

If a new provider reports failure differently, the adapter changes. If the application-level outcome remains the same, the workflow does not need to know. If a repository implementation changes from one storage mechanism to another, callers can continue to express the same data need rather than adopting a new persistence vocabulary.

That is the practical value of an application-level contract.

It gives the application something stable to depend on while letting infrastructure evolve behind it.

The result is not always a perfectly pure boundary. Real systems contain exceptions, tradeoffs, and transitional states. Sometimes a technical detail genuinely matters to a workflow. But that should be a deliberate part of the contract, not an accidental leak.

Refactoring toward application-level contracts is therefore less about hiding infrastructure completely and more about deciding what meaning should cross the boundary.

The application does not need to be ignorant.

It needs to be protected from details that do not belong to its decisions.

11. Keeping Infrastructure Honest at the Edges

Infrastructure should not be treated as unimportant.

It is often where the system becomes real. Data has to be stored. Messages have to be sent. Files have to be read. External services have to be called. Provider responses have to be handled. Failures have to be logged and diagnosed.

Those responsibilities matter.

The goal is not to pretend infrastructure does not exist. The goal is to keep it honest about what it owns.

Infrastructure owns mechanisms. It owns technical integration. It owns provider-specific behavior. It owns query details, serialization, transport, storage, retry mechanics, connection handling, and the translation of external signals into the application’s vocabulary.

That is already a lot.

What infrastructure should not quietly own is the meaning of the workflow.

Keeping infrastructure honest means making the edge explicit. The adapter, repository, or provider implementation should be the place where technical detail is understood and translated. The application should not have to repeatedly rediscover those details in every workflow that depends on them.

For example, a mail sender implementation may know about SMTP responses, API provider payloads, authentication failures, timeouts, and provider diagnostics. That is appropriate. But the sender boundary should return something the application can use, such as whether delivery succeeded, failed, was rejected as invalid, or could not be completed temporarily.

The same principle applies to persistence.

A repository may know about SQL, includes, indexes, projections, transactions, and database-specific exceptions. But the repository boundary should allow the application to express the data it needs without turning the workflow into a database conversation.

The edge should be technical on the inside and application-oriented on the outside.

That is what makes the boundary useful.

One practical way to keep infrastructure honest is to look at the public contract. Method names, parameters, return types, and exceptions all reveal what the boundary is asking callers to know. If the contract exposes too many provider concepts, the infrastructure is leaking outward. If it hides every meaningful outcome behind a vague success or failure, it is not translating enough.

A good contract usually sits between those extremes.

It gives the application stable outcomes. It preserves technical detail for diagnostics. It lets infrastructure change without forcing unrelated workflow code to change with it.

Another way to keep infrastructure honest is to keep provider-specific tests close to the provider-specific code. If an API provider can return a rate-limit response, the adapter should have tests for how that response is handled. If a database exception should become a conflict, the repository or persistence adapter should protect that mapping. The application service should not need to repeat those provider-specific arrangements in every workflow test.

That testing boundary reinforces the design boundary.

Infrastructure also needs honest naming.

A class named as a sender, repository, storage adapter, or provider integration should do that work clearly. If it starts deciding user-facing behavior, domain policy, or workflow compensation, the name may still sound technical while the responsibility has expanded beyond the edge.

That is often how leakage returns.

The code still lives in infrastructure, but infrastructure is now deciding more than mechanisms. It has become a quiet owner of application behavior.

Keeping infrastructure honest requires regular attention. New provider features, operational fixes, emergency patches, and performance improvements can all create pressure to let technical details escape. Some of those tradeoffs may be necessary, but they should be visible decisions, not accidental habits.

A healthy boundary does not make infrastructure invisible.

It makes infrastructure dependable.

Application code can trust it to handle technical complexity, translate external outcomes, and preserve useful diagnostics. Infrastructure code can stay focused on the outside world without becoming the hidden place where the application’s meaning is decided.

That is the balance to aim for.

Keep infrastructure strong at the edges, but keep the application’s language and decisions protected inside them.

12. Closing

Infrastructure leakage is easy to underestimate.

It does not always arrive as an obvious architecture violation. Sometimes there is no forbidden project reference, no direct dependency on a concrete provider, and no class that clearly belongs somewhere else. The structure may look reasonable, while the language of infrastructure has still started to shape the application.

That is why the boundary needs to be understood as more than a dependency rule.

A good boundary does not only hide concrete implementations. It also protects application code from concepts it should not need in order to express the workflow. It keeps SQL, SMTP, HTTP status codes, storage SDKs, framework options, provider payloads, and technical exceptions close to the infrastructure that understands them.

The application should usually depend on application-level meaning.

Was the request valid? Was the item found? Did delivery fail? Was there a conflict? Is the dependency temporarily unavailable? Can the workflow continue?

Those are the kinds of questions application code should be able to ask without translating the outside world every time.

Infrastructure still matters. It may contain the most difficult and failure-prone code in the system. It has to handle providers, networks, databases, files, queues, credentials, timeouts, diagnostics, and all the small differences between environments. None of that is simple.

But that complexity should be concentrated at the edge.

The edge should understand the technical detail, translate it, preserve what is needed for diagnostics, and return outcomes the application can reason about deliberately. When that translation does not happen, infrastructure details begin to spread. They appear in application services, tests, result types, configuration checks, and workflow branches.

At that point, the system becomes harder to change.

A new provider affects workflow code. A database change affects application tests. A framework upgrade reaches places that should not have known about the framework. A technical failure mode becomes part of the application contract without anyone deliberately deciding that it should.

That is the real cost of infrastructure leakage.

It makes the application less stable because it ties application decisions to technical mechanisms that are likely to change.

Keeping infrastructure at the edge is not about purity for its own sake. It is about protecting the language of the application. It is about letting workflows describe what the system is trying to do, while infrastructure handles how the outside world is contacted, interpreted, and adapted.

Good architecture does not pretend that infrastructure does not exist.

It gives infrastructure a clear and important role. It lets infrastructure be technical, detailed, and provider-aware where that is appropriate. But it prevents those details from becoming the normal decision language of the application.

When that balance is right, application code becomes easier to read. Tests become lighter. Provider changes become less invasive. Technical details remain available for diagnostics without taking over the workflow.

Infrastructure should make the outside world usable by the application.

It should not make the application speak the language of the outside world.