Architecture Is Mostly About Where Decisions Belong

1. Architecture Is Not Only About Structure

When people talk about software architecture, the conversation often starts with structure.

Layers. Projects. Namespaces. Dependencies. Boundaries. Diagrams. Which project references which other project. Which class belongs in which folder. Whether the system follows Clean Architecture, layered architecture, vertical slices, modular monolith principles, or something else.

Those things matter.

Structure gives the codebase shape. It helps developers understand where to look, where to add new behavior, and which dependencies are allowed. A system without structure quickly becomes difficult to navigate, even when each individual class looks reasonable in isolation.

But structure is only part of architecture.

A system can have all the expected layers and still be difficult to work with. The UI can still know too much. Application services can still become procedural dumping grounds. Domain concepts can still be reduced to passive data. Infrastructure details can still leak into workflows. Configuration can still quietly decide business behavior from the outside.

On paper, the structure may look correct.

In practice, the decisions may be in the wrong places.1

That is where architecture becomes more than a diagram. It is not only about where code is located. It is also about where decisions are made.

Who decides whether an operation is allowed? Who decides what a workflow means? Who decides how data is retrieved? Who decides how an external provider is called? Who decides what message is shown to the user? Who decides whether a failed operation should be retried, ignored, logged, or reported?

Those questions are architectural questions.

They are not always visible from the folder structure. Two systems can have similar project layouts but very different decision placement. In one system, the application layer may clearly own workflow decisions while infrastructure only performs technical work. In another, the same decisions may be scattered across controllers, repositories, UI handlers, configuration files, and helper classes.

The second system may still have layers. But the layers are not doing enough architectural work.

This is why architecture should not be judged only by whether the code appears to be organized. A good structure can make good decisions easier to place, but it does not guarantee that the decisions are placed well. The real test is whether a developer can understand where a particular kind of decision belongs, and whether the codebase makes the wrong placement feel awkward.

Good architecture helps decisions find their natural home.

It makes some choices belong clearly in the UI, some in the application workflow, some in the domain model, and some in infrastructure. It reduces the number of places where the same kind of decision can hide. It makes the system easier to reason about because responsibility and knowledge are kept close together.

That is the angle this article takes.

Architecture is not only about structure. It is mostly about where decisions belong.

2. Every Decision Has a Natural Home

Every non-trivial system contains many decisions.

Some are small. Which label should be shown on a button? Which CSS class should be used for a warning? Which icon should represent a paused state?

Some are larger. Should this operation be allowed? Should this item be included in the result? Should the workflow continue after this failure? Should this rule be enforced before or after data is saved?

Some are technical. Which database query should be used? Which SMTP server should send the message? Which file path should be used for local storage? Which HTTP client configuration should be applied?

Architecture becomes clearer when those decisions have a natural home.

A presentation decision belongs close to the user interface. The UI knows how something should be shown, which interaction is available, and how the user should be guided through a screen. It should not need to own the rules that decide whether the operation itself is valid.

A workflow decision belongs close to the application layer. The application service knows what steps make up the use case. It can coordinate validation, domain behavior, persistence, external calls, and result handling. It should not need to know the internal details of how a repository queries the database or how a mail provider sends a message.

A domain decision belongs close to the domain concept that gives it meaning. If a rule is part of what an entity or value object means, it should not be scattered across UI handlers, controllers, or infrastructure code. The rule should live where the language of the business or product still makes sense.

A technical decision belongs close to infrastructure. Infrastructure knows how to talk to databases, filesystems, APIs, queues, mail servers, and other external mechanisms. It should decide how those things are performed, but not why the application needs them or what the business outcome should mean.

This is not about creating rigid walls for their own sake. It is about keeping knowledge close to the responsibility that needs it.

When a decision is placed in its natural home, the code becomes easier to reason about. A developer knows where to look. The surrounding code has the right vocabulary. The decision can change for the right reasons without forcing unrelated parts of the system to change with it.

When a decision is placed in the wrong home, the code may still work, but it becomes harder to understand.

A controller that decides business policy becomes more than a transport boundary. A repository that decides workflow meaning becomes more than persistence. A UI component that decides validation ownership becomes more than presentation. An infrastructure adapter that decides whether the user should see a warning becomes more than a technical detail.

Each of those decisions may seem harmless when it is added. But over time, the system starts to lose its shape. Developers can no longer predict where a certain kind of decision belongs. Similar rules appear in different places. Changes become harder because the responsibility is no longer clearly owned.

That is why the question “where should this code go?” is often less useful than “where should this decision be made?”

The code should go where the decision belongs.

That does not always give an immediate answer. Some decisions sit near boundaries. Some belong partly in one layer and partly in another. Some need to be split so that policy, workflow, and mechanism are not forced into the same class. But asking the question still helps. It moves the discussion away from folder names and toward responsibility.

Good architecture gives decisions a natural home. It makes the placement of behavior feel less arbitrary, and it gives the system a clearer way to grow.

Diagram showing where different architectural decisions belong: UI for presentation, application for workflow, domain for meaning, infrastructure for technical mechanisms, and configuration for shaping behavior.

Where decisions belong: UI owns presentation, application code owns workflow, domain code owns meaning, infrastructure owns technical mechanisms, and configuration shapes behavior without owning the decisions.

3. What Happens When Decisions Drift

Decisions do not usually move to the wrong place all at once.

They drift.

A small condition is added to a UI handler because it is convenient. A repository gets an extra parameter because the application needs a slightly different result. A helper method grows a small rule because several callers need the same check. A configuration value starts as a technical option, but slowly becomes a way to change product behavior. An infrastructure adapter catches an exception and quietly decides what the workflow should do next.

Each change may be understandable on its own.

The problem appears over time.

The system starts to contain decisions in places that were not meant to own them. The UI begins to know policy. Repositories begin to know workflow. Infrastructure begins to know business meaning. Application services begin to collect details that belong deeper or further out. Helper classes begin to hide rules that no one clearly owns.

The code may still work. The tests may still pass. The structure may still look acceptable from a distance.

But the system becomes harder to reason about.

One symptom is duplication. When a decision has no clear home, it is easy for the same rule to appear in several places. A validation check may exist in the UI, in an application service, and again near persistence. A filtering rule may appear in a repository query and then be repeated in memory by the caller. A status decision may be calculated in one workflow and slightly differently in another.

Another symptom is surprise. A developer changes one part of the system and discovers that a related decision was hiding somewhere else. A screen behaves differently because a repository silently filters something. A background process behaves differently because it does not pass through the same UI rule. A provider change affects workflow behavior because the provider adapter was deciding more than it should.

Those surprises are expensive.

They create a kind of uncertainty that spreads through the codebase. Developers stop trusting that a decision is made in one predictable place. Before changing anything, they have to search widely, inspect unrelated classes, and mentally reconstruct how the system really behaves.

That is one of the hidden costs of decision drift.

It increases the amount of code a developer must understand before making a safe change. The change itself may be small, but the investigation becomes large because the responsibility is unclear.

Decision drift also makes boundaries weaker.

A boundary is not useful only because it separates files or projects. It is useful because it limits what each part of the system is allowed to decide. If the UI can decide policy, the application layer can decide persistence details, and infrastructure can decide workflow meaning, then the boundary still exists physically but not architecturally.

The system has layers, but the decisions have crossed them.

This often leads to defensive development. New code is added near the place where the problem is visible rather than the place where the decision belongs. A special case is added to the UI because the UI shows the wrong thing. A repository condition is added because the query returns too much. A provider adapter maps an error directly to a user-facing outcome because it is close to the technical failure.

Again, each fix may seem reasonable.

But each one makes the next change a little harder.

Good architecture tries to prevent that drift. It gives decisions a place to return to. When a rule appears in the wrong layer, the codebase should make that feel slightly uncomfortable. When a workflow decision starts leaking into infrastructure, the design should make the mismatch visible. When a presentation decision starts carrying policy, the surrounding code should make the responsibility feel misplaced.

That does not happen automatically. It comes from treating decision placement as part of the design, not as an afterthought.

When decisions drift, architecture becomes harder to see in the code. When decisions have clear homes, the system becomes easier to reason about, change, and trust.

4. The UI Should Decide Presentation, Not Policy

The user interface has an important responsibility.

It should make the system understandable and usable. It should decide how information is presented, which controls are available, which messages are shown, how validation feedback is displayed, and how the user is guided through the workflow.

Those are real decisions.

A UI that presents the wrong information, hides the wrong action, or gives unclear feedback can make a good underlying system feel confusing. Presentation decisions matter because they shape how the user experiences the application.

But presentation is not the same as policy.

The UI may decide that a disabled button should look muted. It may decide that a warning should appear near a field. It may decide that a confirmation message should be shown after a successful operation. It may decide that a validation summary should appear at the top of a form.

Those decisions belong in the UI.

But the UI should not usually be the place that decides whether the operation is allowed, whether the business rule is satisfied, whether a workflow may continue, or whether a particular state transition is valid.

That kind of decision has a different weight.

When policy lives in the UI, it often becomes too easy to bypass. A button may be disabled in one screen, but another workflow may call the same application service directly. A client-side check may prevent one invalid action, but a background job or API endpoint may not use the same screen. A view model may reject a state change, but another caller may construct the same request without that protection.

The rule exists, but only where the user happens to click.

That is rarely enough.

A common symptom is duplicated validation. The UI checks that a value is allowed so it can guide the user early. Then the application layer checks again because it cannot trust the UI. Then the domain model may check again because the rule is part of the meaning of the object. Some duplication of feedback can be reasonable, but duplication of ownership is not*.*

The UI can help the user avoid mistakes. It should not be the only place where important rules are enforced.2

This distinction also matters when the UI changes.

A desktop screen may later become a web page. A form may be replaced by an import process. A manual workflow may be automated. If the policy belongs to the UI, each new entry point has to rediscover and reimplement the same rules. If the policy belongs deeper in the system, the UI can change without moving the meaning of the operation.

That makes the system easier to evolve.

The UI should therefore ask for decisions more often than it owns them. It can ask the application layer whether an operation is available. It can show the result of validation. It can present failure messages based on structured outcomes. It can guide the user toward the next step.

But the decision itself should live where the responsibility belongs.

The UI decides how to present a disabled action. The application or domain code decides whether the action is allowed. The UI decides how to show validation feedback. The application or domain code decides whether the request is valid enough to process. The UI decides how to display the result. The workflow decides what the result means.

Keeping that separation clear makes the UI simpler and the rest of the system safer.

The UI remains important, but it is important for the right reason. It becomes the place where decisions are expressed to the user, not the place where all important decisions are made.

5. Application Services Should Decide Workflow, Not Details

Application services often sit in the middle of the system.

They receive a request from the outside, coordinate the work required to handle it, and return a result the caller can understand. They may validate input, load data, call domain behavior, save changes, send messages, publish events, or ask infrastructure to perform some external action.

That position makes them important.

It also makes them easy to overload.

Because application services coordinate many parts of the system, they can slowly become the place where every decision is placed. A small persistence detail is added because the service already calls the repository. A formatting decision is added because the service already prepares a response. A provider-specific rule is added because the service already calls the provider. A domain rule is added because the service already has all the data.

Before long, the application service is no longer only coordinating the workflow. It is also deciding business meaning, technical mechanism, presentation detail, and infrastructure behavior.

That is too much responsibility.

The natural home of an application service is the workflow decision. It should decide the sequence of the use case: what needs to happen first, what must be checked before continuing, which domain operation should be invoked, which repository should be used, which external dependency should be called, and what kind of result should be returned.

Those are application-level decisions.

But the application service should not need to know how the repository builds its query, how the mail sender talks to its provider, how an entity enforces its invariants, or how the UI will display the final message. Those decisions belong elsewhere.

This distinction keeps the application layer useful without making it too powerful.

For example, an application service may decide that feedback should be validated, stored, and then sent through a configured sender. It may decide that if validation fails, the workflow should stop and return a validation result. It may decide that if sending fails, the operation should return a delivery failure. But it should not need to know the SMTP details, the HTML formatting of a user message, or the internal rules that make the feedback entity valid.

The application service should know the steps. It should not own all the details inside each step.

This is where many systems become procedural. The service starts as a clean use-case coordinator, but gradually absorbs more and more behavior from the objects and adapters around it. The result is often a long method that reads like a script for the entire system.

It may still be easy to follow at first. But as the workflow grows, the service becomes harder to change safely because many different kinds of decisions are now mixed together.

A good application service keeps the workflow visible while pushing specialized decisions to the places that own them.

It asks the domain model to make domain decisions. It asks repositories to retrieve and persist data. It asks infrastructure adapters to perform technical work. It maps the result into an application-level outcome that the caller can use.

That does not make the application service passive. It still owns the orchestration. It still decides which collaborators are involved and how the use case proceeds. But it does not need to become the owner of every rule, every query, every provider detail, and every presentation decision.

That restraint is important.

An application service that decides too little becomes a thin pass-through with no clear purpose. An application service that decides too much becomes a procedural dumping ground. The useful middle is where the application service owns the workflow but respects the responsibilities of the parts it coordinates.3

When that balance is right, the application layer becomes one of the clearest places in the system. It shows what the use case does without hiding important decisions in the wrong places or pulling every detail into itself.

6. Domain Code Should Decide Meaning, Not Mechanics

Domain code should be where the important meaning of the system is protected.

It is the place where concepts are more than data. An order is not only a row in a database. A license is not only a set of fields. A subscription is not only a date range. A message is not only a collection of strings. Each of those concepts may carry rules about what is valid, what can change, what a state means, and which operations are allowed.

Those decisions belong close to the domain concept itself.

If a rule defines what a concept means, it should not be scattered across controllers, UI handlers, repositories, or application services. It should live where the language of the concept is still clear. That makes the rule easier to find, easier to test, and harder to bypass accidentally.

But domain code should not decide everything.

It should decide meaning, not mechanics.

A domain object may know whether a state transition is allowed. It may know whether a value is valid according to the business language. It may know whether an operation changes the meaning of the object. It may know that a license cannot be activated after it has expired, or that a published article should not be edited in the same way as a draft.

Those are domain decisions.

But the domain object should not know how the article is stored, how a payment provider is called, how an email is sent, how a database transaction is opened, or how a user interface shows an error message. Those are mechanical or infrastructural concerns. They may be necessary for the system to work, but they are not part of the domain meaning itself.

This distinction protects the domain from becoming coupled to the outside world.

When domain code starts depending on infrastructure, the meaning of the system becomes harder to keep stable. A rule that should be about the product starts to depend on a database query, an HTTP client, a framework type, or a provider-specific response. The domain model may still contain business words, but its decisions are now mixed with technical mechanisms.

That makes the code harder to reuse and harder to reason about.

It also makes tests heavier. A rule that should be testable as a simple domain behavior may suddenly require a database, a mock provider, a web framework, or configuration. The more mechanics the domain knows, the harder it becomes to test the meaning directly.

A better design keeps the domain focused*.*

The application service can load the necessary data. Infrastructure can retrieve it. Configuration can provide technical options. The UI can show the outcome. But the domain code should decide the meaning of the operation once it has the information it needs.

For example, an application service may load an article and ask whether it can be scheduled for publishing. The repository decides how to retrieve the article. The application service decides where that check belongs in the workflow. But the domain concept should own the meaning of whether the article is in a state that can be scheduled.

That keeps the rule close to the concept it protects.

This does not mean every system needs a large domain model. Some applications are mostly workflow and data movement. Some rules are simple enough to live in application services without creating unnecessary abstractions. But when a rule expresses the meaning of a core concept, placing it in the domain keeps the system more honest.

The question is not whether code looks “domain-driven” from the outside.

The question is whether the decisions that define the meaning of the system are placed where that meaning can be understood, protected, and changed deliberately.

Domain code should not know how everything works. It should know what things mean.

7. Infrastructure Should Decide How, Not Why

Infrastructure code is where the system meets the outside world.

It talks to databases, filesystems, email servers, HTTP APIs, queues, clocks, configuration stores, identity providers, payment systems, and other external mechanisms. It handles details that are necessary, but often not central to the meaning of the application.

That makes infrastructure important.

It also makes it dangerous when it starts deciding more than it should.

The natural responsibility of infrastructure is the “how”. How is data stored? How is a query executed? How is an email sent? How is a file opened? How is an HTTP request built? How is a provider response parsed? How is a message placed on a queue?

Those are infrastructure decisions.

But infrastructure should not usually decide the “why”. Why should this operation be allowed? Why should this workflow continue? Why does this failure matter to the user? Why should this item be included from a business point of view? Why should one outcome be treated as a validation problem and another as a conflict?

Those are application or domain decisions.

When infrastructure starts deciding why, the system becomes harder to reason about. A repository may silently apply a business rule that the caller does not know about. A mail sender may decide which user-facing message should be shown after a failed delivery. A payment adapter may translate provider behavior directly into workflow policy. A file storage implementation may decide whether a missing file means “not found”, “invalid state”, or “operation failed”.

Those decisions may look convenient when they are added, because infrastructure is close to the technical facts. It sees the database response. It sees the provider error. It sees the file system exception. It knows whether a request timed out or a record was missing.

But seeing the technical fact is not the same as owning the application meaning.

A repository can know that no row was returned. The application decides what that means for the use case. An HTTP adapter can know that a provider returned a specific status code. The application or domain layer decides whether that becomes a retriable failure, a conflict, a validation result, or an unexpected problem. A mail sender can know that delivery failed. The workflow decides what should happen next.

This separation keeps infrastructure replaceable*.*

If the system moves from SQL Server to another database, from SMTP to an API-based mail provider, or from local files to cloud storage, the technical mechanisms will change. The application meaning should not have to change with them. The caller should still be able to reason in terms of the application’s vocabulary, not the provider’s vocabulary.

That does not mean infrastructure should be ignorant.

Infrastructure often has to translate technical outcomes into stable application-level results. It may catch provider-specific exceptions, inspect status codes, map database errors, and preserve diagnostic details for logging. But that translation should respect the boundary. It should expose the meaning the application has agreed to use, not leak every provider decision into the workflow.

For example, an infrastructure adapter may map several provider-specific mail errors to a DeliveryFailed result. It may include a diagnostic message or provider code for logging. But it should not decide whether the UI should show a warning, whether the user should retry, or whether the workflow should compensate in some other way.

Those responses belong to the caller*.*

The same principle applies to repositories. A repository should decide how to retrieve the data the application asked for. It may encapsulate query details, include necessary relationships, and protect persistence concerns from leaking outward. But it should be careful about silently owning business policy. If a query filters out records because of a technical requirement, that may belong in the repository. If it filters out records because of a workflow rule, that rule may belong closer to the application or domain.

The difference can be subtle.

That is why infrastructure boundaries require discipline. Infrastructure code is often the easiest place to add a practical fix, because it is close to the external behavior. But practical fixes can become hidden policy when no one asks whether the decision belongs there.

Good infrastructure is not weak. It is focused*.*

It knows how to perform technical work reliably. It hides provider complexity. It preserves useful diagnostics. It translates external mechanisms into application-level concepts. But it does not take ownership of decisions that belong to the workflow or the domain.

Infrastructure should make the outside world usable by the application. It should not quietly become the place where the application’s meaning is decided.

8. Configuration Should Shape Behavior Without Owning It

Configuration is a useful way to make a system adaptable.

It can decide which connection string to use, which feature flag is enabled, which timeout applies, which mail provider is active, which storage location should be used, or which external endpoint the application should call.

Those are important decisions.

Without configuration, too many details would have to be hardcoded. Every environment change, deployment difference, or operational adjustment would require a code change. That would make the system less flexible and harder to run safely.

But configuration should usually shape behavior, not own it.

There is a difference between configuring a decision and becoming the place where the decision lives.

A setting can say which sender implementation should be used. It should not contain the workflow meaning of what sending represents. A setting can say how many days a token remains valid. It should not be the only place that understands what token validity means. A setting can enable or disable a feature. It should not become a hidden replacement for the application logic that decides whether the feature is allowed in a particular situation.

Configuration is most useful when it supplies values to decisions that are owned elsewhere.

For example, a workflow may decide that feedback should be submitted through the configured delivery mechanism. Configuration can decide whether that mechanism is SMTP, mailto, or an API provider. But the workflow still owns the meaning of feedback submission. The sender implementation still owns the technical delivery details. Configuration only chooses among supported options.

That is a healthy responsibility.

The problem starts when configuration becomes a place for hidden policy.

A boolean flag may begin as a simple operational switch, but later decide whether a business rule applies. A numeric threshold may begin as a technical tuning value, but later define a product rule that no one can find in the code. A string value may begin as a provider option, but later become a workflow mode with several special cases.

The system may still work, but the decision has become harder to see.

Configuration files are often read differently from code. Developers may not search them when trying to understand behavior. Tests may not cover all configured combinations. Operational values may change without the same review as code changes. If important policy lives there, the system can behave differently without the architecture making that difference visible.

That does not mean configuration should be avoided.

It means configuration should be treated as part of the design.

When a setting changes behavior, the code should make it clear what behavior is being shaped. A feature flag should point to a clear application decision. A provider option should select a known implementation. A timeout should be used by infrastructure or workflow code that still owns the meaning of failure. A threshold should be named in a way that reveals whether it is technical tuning or business policy.

Naming matters here.

SmtpHost is clearly technical. MaxRetryAttempts may be operational. AllowPublishingWithoutApproval is closer to policy. UseLegacyValidationRules may indicate a workflow or domain decision that deserves more explicit modeling. The more a setting sounds like a rule, the more carefully its ownership should be considered.

Configuration can also make testing harder if it owns too much.

When important behavior depends on combinations of settings, tests have to cover those combinations to make the contract trustworthy. If the setting only selects a provider or supplies a technical value, that is usually manageable. If the setting changes the meaning of the workflow, the test surface grows quickly.

Good configuration keeps flexibility without hiding responsibility.

It allows the application to vary technical choices, environment-specific values, and operational limits. It can influence behavior where variation is expected. But it should not become a shadow architecture where important decisions are defined outside the code paths that explain them.

The question is not only “can this be configured?”

The question is “should this decision live in configuration, or should configuration only provide a value to a decision owned somewhere else?”

When configuration shapes behavior without owning it, the system remains flexible without becoming mysterious.

9. Tests Reveal Whether Decisions Are in the Right Place

Tests often reveal architectural problems before the structure does.

A system may look well organized from the project layout. The folders may be named clearly. The dependencies may point in the expected direction. The classes may appear to belong to the right layers.

But when the tests are difficult to write, they often tell a different story.

A test that should verify a simple rule may require a web framework, a database, a configuration file, a mocked HTTP client, and several unrelated collaborators. A test that should describe a workflow may have to inspect UI state. A test that should verify application behavior may depend on provider-specific exceptions. A test that should exercise domain meaning may need infrastructure to be present.

That friction is information.

It may mean the decision being tested is not placed where it belongs.

If a domain rule is difficult to test without infrastructure, the domain may know too much about mechanics. If a workflow test requires a real UI, the UI may own too much of the workflow. If a repository test has to assert business meaning, the repository may be deciding more than data access. If a provider adapter test has to verify user-facing behavior, infrastructure may be carrying policy.

The test is not only testing the behavior. It is exposing the shape of the design.

Good decision placement usually makes tests more direct.

A presentation decision can be tested near the UI. A workflow decision can be tested through the application service. A domain decision can often be tested without infrastructure. An infrastructure decision can be tested with provider-specific or persistence-specific concerns isolated from the rest of the application.

The test does not need the whole system to prove a small decision.

That is one of the practical benefits of putting decisions in the right place. The tests can speak at the same level as the decision. They do not have to climb through unrelated layers just to reach the behavior they care about.

For example, if a rule says that an article cannot be scheduled before it is approved, the test should not need to click a button to prove that rule. The UI may have its own test for disabling or hiding the action, but the rule itself should be testable where the rule belongs. If the meaning is domain-level, the domain test should be small. If the scheduling workflow owns the check, the application service test should make that visible.

The same applies to infrastructure.

If a mail provider fails, an infrastructure test may verify how the provider-specific failure is mapped. But the application workflow test should usually only need to verify that a delivery failure outcome is handled correctly. It should not need to understand the SMTP exception, the HTTP status code, or the provider payload.

When tests are written at the wrong level, they become fragile.

A small UI change breaks a test for business policy. A provider change breaks a test for workflow behavior. A persistence optimization breaks a test that was really about domain meaning. Those failures may look like ordinary test maintenance, but they often point to decision placement that is too blurred.

This does not mean every test should be isolated or that integration tests are bad.

Integration tests have value because they show that several parts work together. End-to-end tests have value because they verify real user paths. But if every important decision can only be tested through a large integration path, the design may not be giving those decisions a clear home.

A healthy test suite usually has tests at different levels because the decisions themselves live at different levels.

The UI has tests for presentation and interaction. The application layer has tests for workflow. The domain has tests for meaning. Infrastructure has tests for technical integration and mapping. The full system has a smaller number of tests that prove the parts cooperate.

That distribution is not only a testing strategy. It is also an architectural signal.

When tests are easy to place, decisions are often easy to place. When tests feel awkward, slow, or overly dependent on unrelated details, it is worth asking whether the decision itself has drifted.

Tests do more than protect behavior. They show whether the system’s responsibilities are clear enough to verify.4

10. Moving Decisions Is Often the Real Refactoring

Refactoring is often described as improving the structure of code without changing its behavior.

That is true, but it can make refactoring sound smaller than it really is. In many systems, the most important refactoring is not extracting a method, renaming a class, or moving code into a different folder. Those changes may help, but they are often only the visible part of the work.

The real refactoring is moving a decision to the place where it belongs.

A validation rule moves out of the UI and into the application or domain layer. A workflow decision moves out of a repository and into an application service. A provider-specific mapping moves out of the caller and into an infrastructure adapter. A presentation choice moves out of the domain and back into the UI. A configuration value stops acting as hidden policy and becomes an input to an explicit decision.

Those changes improve more than organization.

They improve the system’s honesty.

Before the refactoring, the code may work, but the responsibility is misleading. A class appears to do one thing while quietly deciding something else. A layer appears to have a boundary while allowing decisions to cross it. A helper appears to reduce duplication while hiding ownership of an important rule.

Moving the decision makes the design clearer.

This kind of refactoring often starts with a small discomfort. A test feels harder than it should. A change requires editing a surprising file. A rule appears in two places. A class needs a dependency that does not fit its responsibility. A developer has to ask, “why is this decision being made here?”

That question is valuable.

It points to the part of the design that may need to change.

Sometimes the fix is simple. A condition can move from a UI handler into an application service. A domain rule can move from a procedural workflow into the object it protects. A provider-specific error translation can move behind the provider boundary. A formatting decision can move out of the application service and into the presentation layer.

Other times, the move reveals that the decision needs to be split.

A single piece of code may contain several decisions that belong at different levels. A workflow may decide that an operation should be attempted. Infrastructure may decide how to attempt it. The domain may decide whether the state allows it. The UI may decide how to present the outcome. If all of that is mixed in one method, moving the code as one block may only move the problem.

The better refactoring is to separate the decisions before placing them.

This is why decision placement is a more useful guide than class placement alone. Moving a class from one folder to another may make the project look cleaner, but it does not necessarily change who owns the decision. The code may still carry the same confusion in a new location.

A good refactoring changes the ownership.

After the refactoring, the caller should know less about details it should not own. The domain should know more about meaning that belongs to it. Infrastructure should hide more mechanism. The application service should express the workflow more clearly. The UI should become more focused on presentation.

The result is often quieter code.

There may be fewer special cases in the UI, fewer business rules inside repositories, fewer provider details in application services, and fewer mysterious helper methods that everyone depends on but no one fully owns.

That is the practical value of this kind of refactoring.

It does not only make the code look cleaner. It makes future changes safer because the next developer has a better chance of finding the decision in the place where it naturally belongs.

When refactoring improves decision placement, it improves architecture*.*

11. Good Architecture Makes Decisions Easier to Find

A codebase becomes easier to work with when important decisions are easy to find.

That sounds simple, but it is one of the most practical benefits of good architecture. Developers spend much of their time answering questions before they can safely make changes. Where is this rule enforced? Where is this workflow decided? Where is this value filtered? Where is this provider error translated? Where is this message selected? Where is this state transition allowed or rejected?

If the answer is predictable, the system feels smaller.

The codebase may still contain many files, many classes, and many technical details. But the developer does not have to search everywhere. The architecture narrows the search space because each kind of decision has a natural place to live.

That reduces cognitive load.

A presentation decision should be found near the UI. A workflow decision should be found near the application service. A domain meaning should be found near the domain concept. A persistence mechanism should be found near infrastructure. A provider-specific mapping should be found behind the provider boundary. A configuration value should be found where the system’s variation is made explicit.

When those placements are consistent, developers can build trust in the codebase.

They do not have to wonder whether a business rule is hidden in a button click handler, a repository query, a provider adapter, and a helper class at the same time. They do not have to inspect every caller to understand whether an operation is safe. They do not have to learn the accidental habits of each feature before they can make a reasonable change.

The system teaches them where to look.

This is especially valuable as a codebase grows. Small systems can survive a lot of inconsistency because one developer may still remember where things are. Larger systems cannot depend on memory in the same way. They need the architecture to carry more of the knowledge.

Good decision placement makes that possible.

It does not remove the need to read code, but it makes reading code more directed. A developer can follow the responsibility instead of following every reference. They can ask, “which part of the system should own this decision?” and then look there first.

When the decision is found there, confidence increases.

When the decision is not found there, that absence is also useful. It may indicate that the decision has drifted, that the rule has no clear owner, or that the design needs to make the responsibility more explicit.

In that sense, architecture is not only about arranging code. It is about making the system searchable by responsibility.

The best architectures often feel almost boring in this way. Similar decisions appear in similar places. Workflows are described where workflows belong. Rules are protected where the meaning lives. Infrastructure hides mechanisms instead of spreading them outward. The UI expresses decisions without owning all of them.

There is less surprise.

That does not mean every decision is obvious at first glance. Real systems still contain edge cases, compromises, legacy behavior, and difficult boundaries. But a good architecture gives the developer a reasonable starting point. It makes the first place to look more likely to be the right place.

That is a quiet but important quality.

A codebase with easy-to-find decisions is easier to maintain, easier to test, and easier to explain. It lets developers spend less time hunting for ownership and more time improving behavior.

Good architecture makes decisions easier to find because it makes responsibility easier to see.

12. Closing

Architecture is often easier to discuss in terms of visible structure.

We can point to projects, layers, folders, references, dependencies, diagrams, and naming conventions. Those things matter because they give the system shape. They help people navigate the codebase and understand the intended direction of dependencies.

But structure is not enough by itself.

The deeper question is where decisions belong.

A system can have layers and still let the wrong decisions leak across them. The UI can decide policy. Infrastructure can decide workflow meaning. Repositories can hide business rules. Configuration can become shadow logic. Application services can absorb details that belong elsewhere. Domain code can become coupled to mechanics it should not know about.

When that happens, the architecture may still be visible on paper, but it becomes harder to feel in the code.

Good architecture makes decision placement clearer. It helps presentation decisions stay near the UI, workflow decisions stay near the application layer, meaning stay close to the domain, and technical mechanisms stay in infrastructure. It gives configuration a role without letting configuration become the hidden owner of behavior.

That clarity matters because software changes.

New screens are added. Providers are replaced. Workflows evolve. Rules are refined. Tests are expanded. Features move from manual use to automation. Each change asks the same quiet question: where should this decision be made?

If the architecture gives a good answer, the change becomes easier to reason about.

The goal is not to make every boundary rigid. Real systems contain tradeoffs, shortcuts, and edge cases. Some decisions sit close to more than one responsibility. Some code starts in one place and later needs to move. Architecture is not perfect placement from the beginning.

It is the ongoing discipline of noticing where decisions belong and moving them closer to that place.

That discipline pays off over time.

It reduces surprise. It makes tests easier to write. It makes rules easier to find. It keeps provider details from spreading through workflows. It keeps the UI focused on presentation. It keeps domain meaning from being diluted by technical mechanics.

Most importantly, it makes the system easier to understand.

A good architecture does not merely answer “where should this class go?” It helps answer a more important question: “who should decide this?”

When that question becomes easier to answer, the codebase becomes easier to change.