Application Boundaries Should Protect Intent

1. Boundaries Are Not Just Lines Between Projects

It is easy to mistake structure for a boundary.

A solution may have several projects. The folders may be tidy. The namespaces may look disciplined. Controllers may live in one place, application services in another, infrastructure in another, and models somewhere else. From a distance, the system may look well separated.

But separation on disk is not the same as separation of responsibility.

A boundary is not useful merely because code is placed on different sides of it. It is useful when it protects meaning.1 It decides what kind of knowledge is allowed to pass through and what kind of knowledge must stay local. It keeps the application from being shaped too directly by the tools, frameworks, protocols, and storage mechanisms around it.

That distinction matters.

A system can have an application project and still allow infrastructure concerns to define application behavior. It can have DTOs and still let transport details become workflow rules. It can have repositories and still let persistence models become the language of the application. It can have interfaces and still expose concepts that belong to a specific implementation.

The structure looks separated, but the intent is not protected.

Good application boundaries do more than divide code.

They help preserve what the application is trying to do.

If a user submits a request, the application should not immediately become a collection of HTTP details, database shapes, provider error codes, and framework conventions. Those details matter, but they are not the intent. The intent is the operation the user is trying to perform, the rule the workflow must enforce, the outcome the caller needs to understand, or the decision the system must make.

A boundary should make that intent easier to see.

It should translate incoming details into application language. It should keep technical mechanisms from becoming business concepts. It should prevent external shapes from quietly becoming internal rules. It should help developers answer the question: what is this part of the system responsible for, in the language of the application?

That is why boundaries need judgment.

Adding another project, layer, or interface does not automatically improve the design. Sometimes it only adds distance. A useful boundary is not defined by how many files sit between two parts of the system. It is defined by whether the important decisions remain clear, local, and protected from details that should not own them.

Application boundaries should not only separate code.

They should protect intent.

2. What Application Intent Means

Application intent is what the system is trying to accomplish.

It is not the protocol used to receive a request. It is not the database table used to store data. It is not the provider API used to perform an operation. It is not the framework type that happens to carry input from one place to another.

Those things are mechanisms.

Application intent is closer to the purpose of the operation.

A user wants to create an account. A customer wants to restore access. A license needs to be activated. A payment must be confirmed. A message should be sent. A workflow should reject a request because an expected rule was not satisfied.

Those statements describe intent.

They are written in the language of the application rather than the language of the surrounding technology. They say what the system is doing before they say how the system happens to do it.

That difference is important because application code is where intent should remain visible.

If application code becomes dominated by framework types, provider responses, persistence shapes, transport status codes, or UI concerns, the purpose of the workflow becomes harder to see. The code may still work, but the meaning becomes mixed with the mechanism. Developers then have to separate the intent from the technical details every time they read or change the code.

A good boundary helps prevent that.

It receives technical details from the outside and translates them into something the application can reason about. It turns a request body into a command. It turns a provider response into an application result. It turns a database record into a model that means something inside the workflow. It turns an external failure into an outcome the caller can handle deliberately.

This does not mean the application should pretend technology does not exist.

The system still needs HTTP, persistence, authentication, providers, files, queues, and frameworks. But those mechanisms should support the intent rather than redefine it. They should not become the vocabulary the application uses to describe its own decisions.

Application intent gives the code a center of gravity.

It helps developers see what the workflow is responsible for. It helps tests describe behavior instead of transport details. It helps boundaries decide what should cross and what should remain outside.

When intent is clear, the system is easier to understand.

When intent is protected, the system is easier to change.

Diagram contrasting technical mechanisms with application intent, showing how a boundary translates details such as HTTP requests, database rows, provider responses, and framework exceptions into meaningful application concepts.

Mechanisms describe how the system receives, stores, or performs something; intent describes what the application is trying to accomplish.

3. Why Technical Details Try to Cross the Boundary

Technical details cross boundaries because it is convenient.

A controller already has access to the request. A provider response already contains a useful status. A database entity already has most of the fields the workflow needs. A framework type already represents the user, the file, the message, or the error. Passing that object inward can feel simpler than translating it.

At first, this may not seem harmful.

The code becomes shorter. There are fewer models to create. There is less mapping. The application service can get the data it needs quickly. The workflow can react directly to the exact status, exception, or value returned by the external mechanism.

But convenience can quietly change the shape of the application.2

A provider status begins to determine workflow behavior. A database column name becomes part of the application language. An HTTP concept appears in code that should be describing a use case. A UI decision influences validation. A framework exception becomes something application tests need to understand.

The boundary still exists in the folder structure.

But the technical detail has crossed it.

This usually happens gradually. One small shortcut is added because the case is simple. Another is added because the previous shortcut is already there. A third is added because translating the detail now feels like unnecessary ceremony. Over time, the application begins to speak in the language of the mechanisms around it.

That makes change harder.

If a provider changes its status codes, application logic may need to change. If a database shape changes, workflow code may be affected. If the transport mechanism changes, tests may fail for reasons that have little to do with application behavior. The application becomes coupled not only to what it needs to know, but to how surrounding technologies happen to express it.

Technical details also cross boundaries because they often look authoritative.

A provider response can feel like the source of truth. A database model can feel like the natural shape of the data. A framework type can feel like the correct abstraction because it is official, documented, and widely used. But external authority is not the same as application meaning.

The application still needs its own language.

A good boundary does not reject technical details. It receives them, understands them, and translates them before they become part of the application’s decisions. It asks what the workflow actually needs to know, and it protects the rest of the system from everything else.

That translation may feel like extra work.

But without it, technical convenience becomes architectural coupling.

4. A Boundary Should Translate, Not Leak

A useful boundary translates.

It does not simply pass external details inward and hope the rest of the system knows what to do with them. It receives those details at the edge, understands what they mean, and converts them into language the application can use deliberately.

That translation is one of the main reasons the boundary exists.

A provider may return status codes, response bodies, headers, error messages, retry hints, or exception types. A transport layer may receive query strings, route values, form data, files, and HTTP status concerns. A database may expose rows, columns, keys, constraints, and persistence-specific relationships.

Those details may be necessary.

But they are not automatically application concepts.

The application usually needs a smaller and more meaningful version of them. It may need to know that a payment was confirmed, that a license could not be activated, that a user request was invalid, that a provider was temporarily unavailable, or that a record could not be found. Those are outcomes the workflow can reason about.

A leaking boundary skips that translation.

It lets the raw provider status move into application logic. It lets the database shape define the workflow. It lets framework types appear in places where the application should be describing intent. It forces callers to understand details that belong to the mechanism, not to the use case.

The result is subtle coupling.

The code may still compile cleanly. The layers may still be present. The names may still look organized. But the application has started to depend on the wrong language. It is no longer protected from changes in the technical surroundings.

A translating boundary creates a buffer.3

It turns external detail into internal meaning. It reduces what the application needs to know. It allows technical mechanisms to change without forcing every caller to understand those mechanisms. It also gives tests a clearer target because the behavior can be tested in application terms rather than provider or framework terms.

This does not mean translation should be excessive.

Not every value needs a new type. Not every small operation needs a mapper. Not every boundary needs a large formal structure. Translation becomes useful when it prevents an external detail from becoming an internal decision.

The question is not whether translation adds code.

It does.

The better question is whether that code protects intent.

If a small amount of translation keeps provider details, persistence assumptions, transport concerns, or framework types from spreading through the application, it is not ceremony. It is part of the boundary doing its job.

Diagram comparing a leaking boundary that passes technical details inward with a translating boundary that converts external mechanisms into application language.

A useful boundary does not merely pass details inward; it translates external mechanisms into application language.

5. Application Code Should Describe What the User Is Trying to Do

Application code should make intent visible.

When someone reads an application service, handler, or workflow, they should be able to understand what the user or caller is trying to accomplish. The code should describe the operation in application terms before it exposes the technical path used to perform it.

That sounds simple, but it is easy to lose.

A workflow can gradually become a sequence of technical steps. Read from request. Validate DTO. Load entity. Call provider. Map response. Save record. Return status. Each step may be necessary, but the larger purpose can disappear behind the mechanics.

The code tells the reader what happens.

But not clearly enough why it happens.

A better application boundary helps preserve the intent of the operation. It turns incoming data into something that represents the user’s request. It names the workflow in terms of the action being attempted. It returns outcomes that callers can understand without knowing every technical detail behind them.

This matters because application code is often where business change appears first.

A rule changes. A new case must be supported. A request should be rejected for a clearer reason. A provider response should be interpreted differently. A workflow should treat one outcome as recoverable and another as final.

Those changes are easier when the code already speaks the language of the workflow.

If the application code is dominated by transport details, persistence shapes, framework types, or provider-specific names, the real intent becomes harder to change safely. Developers first have to rediscover which technical steps correspond to which application decisions.

Intent-revealing code reduces that burden.

It lets developers see the decision being made. It makes tests easier to read because the behavior can be described in the same terms as the workflow. It also makes boundaries easier to enforce because the application has its own vocabulary instead of borrowing one from every surrounding mechanism.

This does not mean application code should be vague or detached from reality.

It still needs to call repositories, adapters, services, validators, and other collaborators. But those collaborators should appear in support of the workflow, not as a replacement for it. The code should not read as if the user wanted to execute infrastructure operations. It should read as if the user wanted to accomplish something meaningful in the system.

A good application boundary protects that meaning.

It allows technical mechanisms to help the workflow without becoming the workflow.

When application code describes what the user is trying to do, the system becomes easier to understand, easier to test, and easier to change.

6. Infrastructure Should Support Intent Without Redefining It

Infrastructure is necessary.

Applications need databases, file systems, message queues, external providers, authentication systems, email services, cloud storage, logging frameworks, and transport mechanisms. Without infrastructure, most application workflows cannot do anything useful.

But infrastructure should support intent.

It should not redefine it.

This distinction is easy to miss because infrastructure often has strong opinions. A provider has its own terminology. A database has its own shape. A framework has its own lifecycle. A transport mechanism has its own status codes, exceptions, headers, and conventions.

Those opinions are real.

But they are not automatically the application’s opinions.

A payment provider may distinguish between several technical states, but the application may only need to know whether the payment can be treated as confirmed, pending, failed, or requiring follow-up. A database may store data in a shape optimized for persistence, but the workflow may need a different shape to express its decisions. An email service may return provider-specific failures, but the application may need outcomes that describe what the caller can do next.

The boundary is where that difference should be handled.

Infrastructure can know about the provider, the database, the file system, or the framework. It can deal with awkward protocols, retries, serialization, authentication, connection strings, timeouts, and technical failures. But before those details reach the application, they should be translated into concepts that preserve the workflow’s intent.

Otherwise, infrastructure begins to define the application from the outside.

The workflow starts asking questions in provider language. Application results start reflecting technical categories instead of caller decisions. Tests start depending on framework objects. Error handling starts following the structure of the mechanism rather than the meaning of the operation.

That makes the application harder to change.

When infrastructure redefines intent, replacing or adjusting the infrastructure becomes much more expensive. The technical detail is no longer contained. It has become part of the application’s vocabulary.

Good architecture avoids that by giving infrastructure a clear role.

Infrastructure should know how to perform the operation.

The application should know why the operation matters.

That separation is not about treating infrastructure as unimportant. It is about preventing technical mechanisms from taking ownership of decisions they should only support.

When infrastructure supports intent without redefining it, the application remains easier to read, test, and evolve.

7. UI and Transport Concerns Should Not Become Workflow Rules

UI and transport concerns often sit close to the user.

That makes them important.

The UI presents choices, collects input, shows errors, and helps the user understand what is possible. The transport layer receives requests, binds data, handles routes, manages status codes, and connects the outside world to the application.

But neither of them should quietly become the owner of workflow rules.

This can happen when a controller decides too much. It can happen when a Razor Page handler contains rules that belong to the application. It can happen when a UI field determines behavior directly instead of being translated into an application request. It can happen when HTTP status codes, query strings, route names, or button choices begin to shape the workflow itself.

At first, that may feel natural.

The request arrives through the UI or transport layer, so it is tempting to handle the rule there. A value is missing, so the handler rejects it. A checkbox is selected, so the controller chooses a path. A route parameter has a certain value, so the endpoint decides what the workflow means.

Some of that is unavoidable.

The edge of the system must validate shape, collect input, and reject requests that cannot even be understood. But there is a difference between checking whether input can be received and deciding what the application should do with meaningful input.

That difference is the boundary.

A UI can express user intent, but it should not own the application decision. A controller can translate an HTTP request into a command, but it should not become the place where workflow behavior is scattered. A transport layer can return a status code, but it should not force the application to think in status codes.

When UI and transport concerns become workflow rules, the application loses clarity.

The same behavior may be repeated in multiple endpoints. A rule may work through one screen but not another. Tests may need to exercise controllers just to verify application behavior. A future change to the workflow may require searching through presentation code, routes, handlers, and application services to find every piece of the decision.

That makes change less local.

Good application boundaries prevent this by translating user input into application intent. The UI says what the user is trying to do. The transport layer carries that request. The application decides what the request means and what outcome should be produced.

That separation keeps the workflow readable.

It also keeps the UI free to change without taking application rules with it. A screen can be redesigned, an endpoint can be renamed, or a transport mechanism can be adjusted without rewriting the meaning of the workflow.

The edge should help the user express intent.

It should not become the place where intent is defined.

8. Persistence Models Should Not Become Application Language

Persistence models are shaped by storage concerns.

They may reflect tables, columns, keys, indexes, relationships, migrations, constraints, serialization rules, or the requirements of an ORM. Those concerns are important. A system needs data to be stored, retrieved, updated, and protected consistently.

But persistence shape is not the same as application intent.

A database model describes how data is kept.

Application code should describe what the system is trying to do with that data.

When the persistence model becomes the application language, the workflow starts to think in terms of storage. Entity names, column names, navigation properties, database relationships, and persistence-specific constraints begin to shape decisions that should belong to the application.

That can make the code feel efficient at first.

There are fewer objects to create. There is less mapping. The same model can be loaded, changed, saved, passed to the UI, returned from APIs, and used in tests. It may look like unnecessary duplication has been avoided.

But the cost appears when the application needs to change for a reason that is not the same as the database.

A workflow may need a concept that does not map cleanly to one table. A result may need to express an outcome rather than a stored state. A rule may depend on meaning that is spread across several records. A persistence optimization may require changing the database shape without changing the application behavior.

If application code speaks directly in persistence terms, those changes become harder to separate.

The database begins to own more meaning than it should.

This does not mean persistence models are bad. They are useful when they are kept close to persistence. They can represent how data is stored, how relationships are loaded, and how records are saved. But they should not automatically become the vocabulary of workflows, results, commands, or application decisions.

A good boundary translates between stored data and application meaning.

It may load a record and create a model the workflow can reason about. It may save data based on an application decision without exposing every persistence detail. It may map between database constraints and meaningful application outcomes. It may hide whether the data came from one table, several tables, a view, a document, or an external source.

That translation protects intent.

It allows the database to change for persistence reasons without forcing the application to change its language. It also allows the application to evolve its workflows without pretending every decision is naturally shaped like stored data.

Persistence is essential.

But it should not define what the application means.

9. Result Types Can Preserve Intent Across Boundaries

Result types can help a boundary preserve intent.4

They do this by giving expected outcomes a clear application-level shape. Instead of allowing raw exceptions, status codes, provider responses, or persistence failures to travel through the system, a result type can describe what the operation means to the caller.

That distinction is important.

A provider may say that a request failed with a particular technical code. A database may say that a record was not found. A transport mechanism may say that a request was invalid. Those details may matter at the edge, but the application often needs to express something more deliberate.

The caller needs to know what happened in terms of the workflow.

Was the command accepted? Was the request rejected for an expected reason? Was the operation impossible because a rule was not satisfied? Was the external system unavailable? Is the caller allowed to retry? Does the user need to correct input? Has the workflow reached a final state?

Those are application outcomes.

A result type can name them directly.

This helps prevent boundaries from leaking technical language. Instead of returning a provider response to application code, an adapter can return a result such as Confirmed, Pending, Rejected, or ProviderUnavailable. Instead of forcing callers to catch several technical exceptions, an application service can return outcomes that describe what the caller is expected to handle.

That makes the boundary more useful.

The result type becomes part of the translation. It turns technical detail into application meaning. It also gives tests a clearer target, because tests can verify expected outcomes without depending on the exact mechanism that produced them.

Result types are especially useful for expected failures.

If an outcome is part of the normal workflow, it should usually be visible in the contract. A caller should not have to discover through documentation, exceptions, or provider-specific behavior that a request can be rejected, delayed, unavailable, or incomplete. The result should make those possibilities explicit.

This does not mean every method needs a custom result type.

A result type is useful when the caller has a meaningful decision to make. If there is only one sensible response, a simpler return value may be enough. But when the operation can produce different expected outcomes, a result type can protect intent by naming those outcomes in application terms.

Good boundaries do not only keep details out.

They also decide what meaning should come through.

A well-shaped result type helps that happen. It allows technical mechanisms to remain technical while giving the application and its callers a clearer language for decisions.

10. When a Boundary Becomes Too Thin

A boundary can exist and still be too thin.

It may have the right name. It may sit in the right project. It may even use an interface. But if it mostly passes technical details through unchanged, it is not protecting much. It becomes a doorway rather than a boundary.

This often happens when the boundary is treated as plumbing.

A controller passes a DTO directly into the application. An application service passes a provider response directly to its caller. A repository returns persistence entities that the workflow changes directly. An adapter wraps an external API but exposes the same terminology, status codes, and failure categories that the provider uses.

The code has a boundary in structure.

But not in meaning.

A thin boundary can feel efficient because it avoids translation. There are fewer types, fewer mappings, and fewer decisions about language. The system appears simpler because the external shape is reused internally.

But that simplicity can be misleading.

If the external shape is allowed to become the internal shape, the application becomes dependent on decisions made elsewhere. A provider can influence workflow language. A database can influence application rules. A transport mechanism can influence result handling. A UI model can become the shape of a use case.

The boundary is present, but it is not doing enough work.

A useful boundary should usually add some meaning. It should reduce what crosses. It should translate details into concepts the application owns. It should make the caller less dependent on the mechanism behind the boundary, not merely hide the class that performs the call.

This does not mean a boundary must always be thick.

Overly elaborate boundaries can create ceremony of their own. If the translation does not protect a decision, clarify intent, or reduce coupling, it may not be worth adding. A boundary should not become a place where code is transformed only because the architecture expects transformation.

The right thickness depends on what the boundary protects.

If the external detail is stable, harmless, and already expressed in application language, little translation may be needed. But if the detail belongs to a framework, provider, persistence model, transport mechanism, or UI shape, the boundary probably needs to do more than pass it along.

A boundary becomes too thin when it stops asking what the application should know.

It lets too much through because passing things along is easier than deciding what they mean.

Good architecture does not measure a boundary by how many layers it contains.5

It measures it by whether intent survives crossing it.

11. Refactoring Boundaries Around Intent

Refactoring a boundary usually starts by noticing where intent has become unclear.

A workflow may be full of provider terminology. An application service may accept a DTO that was shaped for the UI. A repository may return persistence entities that are changed directly by application code. A result may expose technical failure details that callers should not have to understand.

Those are signs that the boundary is no longer protecting enough.

The first step is not necessarily to introduce a large new architecture. It is often better to ask a smaller question: what does the application actually need to know here?

That question can guide the refactoring.

If a controller passes transport data inward, introduce a command or request model that describes the user’s intent. If an adapter exposes provider status codes, translate them into application outcomes. If persistence entities have become workflow language, create a model or method that expresses the decision the workflow needs to make. If technical exceptions are being handled far from the edge, translate them where the mechanism is known.

These changes do not have to be dramatic.

They only need to move meaning back to the right side of the boundary.

A useful refactoring makes the application code read more like the application and less like the surrounding mechanism. It gives decisions names that belong to the workflow. It reduces what callers need to know about external details. It makes tests easier to write in terms of behavior rather than framework or provider behavior.

This kind of refactoring is often easiest to do one seam at a time.

Choose one place where technical language has crossed too far. Add a small translation. Rename the outcome. Move the decision closer to the workflow that owns it. Split a model if it is serving two different purposes. Let the boundary become a little more honest about what it protects.

The goal is not to create perfect separation.

The goal is to restore meaning.

Some details can cross a boundary safely because they are already part of the application’s language. Others should remain local to the mechanism that produced them. Refactoring around intent means learning to tell the difference.

A boundary is improved when a future developer can read the code and understand the operation without first learning the accidental shape of the UI, database, provider, or framework.

That is the practical value of this kind of refactoring.

It makes the system easier to understand because the application is allowed to speak for itself again.

Decision diagram showing what should cross an application boundary unchanged, what should be translated, what should be mapped, and what should remain local to the mechanism.

A boundary protects intent by deciding what may cross unchanged, what must be translated, and what should remain local to the mechanism.

12. Closing

Application boundaries are not valuable only because they separate code.

They are valuable because they protect intent.

A useful boundary helps the application remain clear about what it is trying to do. It keeps technical mechanisms from becoming the language of the workflow. It translates external details into application meaning. It prevents provider responses, persistence models, transport concerns, and UI shapes from quietly owning decisions that belong somewhere else.

That kind of boundary makes the system easier to understand.

A developer can read the application code and see the operation being performed, not only the mechanisms involved in performing it. They can see what the caller is trying to accomplish, what the workflow decides, and what outcomes the application is prepared to return.

That clarity matters over time.

Systems rarely stay still. Providers change. UI flows are redesigned. Persistence models evolve. APIs gain new cases. Workflows become more precise. Expected failures need better names. If the application has allowed external mechanisms to define its language, each of those changes becomes harder to contain.

A good boundary reduces that risk.

It does not make the application independent of infrastructure, transport, persistence, or presentation. No useful application is independent of the mechanisms it uses. But it keeps those mechanisms in supporting roles. They help the application do its work without redefining what that work means.

This is one reason boundaries require judgment.

A boundary that is too thin may leak details. A boundary that is too elaborate may add ceremony. The right boundary is the one that protects the meaning that would otherwise be easy to lose.

That meaning is often the most important part of the system.

Not the framework type. Not the database shape. Not the provider code. Not the transport convention.

The intent.

Application boundaries should protect it.