Architecture Should Make Wrong Code Feel Wrong
Architecture Principles · 2026-06-01
A practical exploration of how good architecture guides developers toward healthy decisions, makes misplaced responsibilities easier to recognize, and reduces the chance that harmful design choices feel natural or convenient.
Architecture Should Make Wrong Code Feel Wrong
1. Introduction: Architecture as Guidance
When developers discuss software architecture, the conversation often focuses on structure: layers, abstractions, services, boundaries, frameworks, patterns, and dependencies. Those things matter, but structure alone does not determine whether an architecture is genuinely helpful to work within.
A system may look clean on diagrams while still making everyday development confusing, fragile, or inconsistent.
One of the most important qualities of a good architecture is not merely that it permits correct solutions. It is that it guides developers toward them naturally.
In a well-structured system, the intended responsibilities tend to feel obvious. Certain kinds of changes fit naturally into specific places. The boundaries between concerns become easier to recognize. Correct usage feels consistent with the shape of the system itself.
By contrast, weak architecture often forces developers to rely heavily on memory, tribal knowledge, or repeated correction from code reviews. Multiple locations may appear equally valid for the same logic. Misplaced responsibilities may compile, pass tests, and even work correctly while still quietly weakening the structure of the system.
This creates an important distinction:
Good architecture does not only make the right code possible. It makes the wrong code feel out of place.
That feeling matters more than it may first appear.
Software systems evolve continuously. New features are added. Bugs are fixed. Developers join and leave teams. Under those conditions, architecture acts less like a static blueprint and more like an environment that shapes everyday decisions. The system either helps developers make coherent choices or leaves them navigating ambiguity repeatedly.
For example:
- Does validation naturally end up in the same kinds of places across the system?
- Do asynchronous operations clearly communicate their lifetime and ownership?
- Is business logic drawn toward the domain model rather than scattered through controllers and UI handlers?
- Do repositories naturally express persistence concerns rather than application workflows?
- Does adding a new feature encourage consistency with the existing design? When the answers are unclear, the architecture may technically exist without actively guiding behavior.
This does not mean a system should become rigid or difficult to extend. Good architectural guidance is usually subtle. It comes from clear naming, understandable abstractions, deliberate boundaries, coherent responsibilities, and APIs that naturally suggest the intended use.
In that sense, architecture is not only about enabling capabilities. It is also about creating pressure toward healthy decisions and friction against unhealthy ones.
A useful architecture therefore reduces the number of moments where developers must stop and ask:
“Where is this logic supposed to go?”
Instead, the structure itself begins answering the question.
2. What “Wrong Code Feels Wrong” Means
“Wrong code feels wrong” does not mean that the compiler rejects every poor design choice. Most architectural problems are not syntax errors. They are decisions that compile, run, and may even pass tests, but gradually make the system harder to understand.
A controller can contain business rules. A UI component can apply filtering. An application service can know too much about persistence. A repository can begin shaping workflow decisions. A domain object can be reduced to a passive data container.
All of these choices may work locally. The problem is that they weaken the expectations that make the system understandable.
When wrong code feels wrong, the surrounding design gives hints that something does not belong. A developer adding business logic to the UI may notice that similar rules are expressed elsewhere. A method signature may make it clear that a layer should ask for a result, not construct it from lower-level details. A repository API may encourage retrieval by intent rather than exposing raw data for callers to filter themselves.
This is not about making the system hostile to change. It is about making responsibility visible.
For example, if publishing rules belong to an Article object or an ArticlePublishingPolicy, then writing those same rules inside a Razor Page handler should feel awkward. The handler should already be shaped around receiving input, invoking a use case, and returning a response. If it starts accumulating business conditions, it should feel like it is fighting the design.
The same principle applies to naming. A type called ArticleRepository suggests persistence behavior. A type called ArticlePublishingPolicy suggests a decision about whether publishing is allowed. A type called ArticleEditorViewModel suggests presentation state. If those names are accurate and consistently used, misplaced code becomes easier to notice.
Wrong code feels wrong when the design creates contrast between the intended responsibility and the misplaced implementation.
This is one reason consistency matters. If each feature uses a different structure, almost any new code can appear acceptable. But when similar responsibilities are handled in similar ways across the system, deviations become visible. A duplicated validation rule, a direct infrastructure dependency, or a misplaced workflow condition stands out because it does not match the surrounding pattern.
That does not mean every deviation is wrong. Sometimes the design needs to evolve. Sometimes a new requirement reveals that an existing boundary is too narrow or that a responsibility belongs somewhere else. But even then, the discomfort is useful. It forces the question:
Is this code in the wrong place, or is the architecture telling us something needs to change?
In that sense, “wrong code feels wrong” is not about rigid rules. It is about feedback. The architecture should help developers notice when a change does not fit, so they can either place it better or intentionally reshape the design.
3. When Architecture Makes Misuse Easy
Some architectural problems persist because the wrong place is also the easiest place.
A screen needs one more condition, and the UI already has the data. A handler needs to hide unpublished records, and loading everything from the repository seems convenient. A service needs a small formatting rule, and adding it inline is faster than finding a better owner. Each change may be understandable in isolation, but the architecture is quietly encouraging misuse.
Architecture that guides developers toward healthy responsibility placement compared to architecture where boundaries leak and misuse feels normal. Click the diagram to open it in full size.
This often happens when important boundaries are present in theory but weak in practice. The solution may contain separate projects, layers, interfaces, and services, but the APIs still make it easy for callers to bypass the intended responsibilities.
For example, a repository that exposes all records without intent-specific methods may encourage callers to perform filtering themselves:
var articles = await _articleRepository.GetAllAsync(ct);
var visibleArticles = articles
.Where(article => article.IsPublished)
.OrderByDescending(article => article.PublishedAtUtc)
.ToList();
This may work, but it makes the caller responsible for knowing what “visible articles” means. If several callers do this, the rule can easily be duplicated or changed inconsistently.
A more guiding API makes the intended use clearer:
var visibleArticles =
await _articleRepository.GetPublishedArticlesAsync(ct);
The difference is not only convenience. The second version gives the visibility decision a clearer home. It makes the correct path easier than the accidental one.
Misuse also becomes easy when abstractions are too vague. A class named ArticleService, ArticleManager, or ArticleHelper may become a natural dumping ground because the name does not strongly communicate what belongs there. If a type can plausibly contain almost anything related to articles, developers will often place almost anything there.
The same risk appears when methods expose technical detail instead of application intent. If higher layers receive IQueryable, file paths, raw HTTP responses, database entities, or infrastructure-specific exceptions, they may begin making decisions that should belong elsewhere. The architecture no longer guides them away from lower-level concerns.
This is why good architecture should not only define where code belongs. It should make the intended path more natural than the unintended one.
That usually comes from:
- APIs that express use cases or responsibilities clearly,
- names that narrow rather than broaden responsibility,
- boundaries that hide details callers should not need,
- and types that make common misuse look awkward.
When misuse is easy, teams compensate with discipline, documentation, and code review. Those still matter, but they are weaker than a design that naturally resists the wrong placement.
A useful question is:
Where would a rushed developer most likely put this code?
If the answer is consistently “in the wrong place,” the architecture may be offering too little guidance.
4. Clear Boundaries as Design Pressure
Clear boundaries do more than organize code after it has been written. They create design pressure while the code is being written.
A boundary is useful when it makes certain responsibilities naturally belong on one side and not the other. The UI presents state and collects input. The application layer coordinates use cases. The domain model protects business meaning. Infrastructure handles persistence, external systems, files, networks, and technical mechanisms.
When those boundaries are clear, misplaced code becomes easier to recognize.
For example, if a Razor Page handler starts deciding whether an article is publishable, the boundary should make that feel questionable. The handler may need to display whether publishing is possible, and it may initiate the publish operation, but the rule itself should probably live closer to the domain or application policy.
Likewise, if a domain object starts depending on a repository, HTTP client, configuration provider, or logging abstraction, that should create immediate discomfort. The domain model should express business behavior using information already available to it. If it needs infrastructure access to make a decision, the design may be pulling lower-level concerns into a place where they do not belong.
Clear boundaries therefore act as a kind of architectural feedback. They do not prevent every mistake, but they make mistakes more visible.
This is especially important in layered systems because many boundary violations are technically easy. A reference can be added. A parameter can be passed. A helper can be called. A query can be shaped in the wrong layer. Without design pressure, the system gradually accepts these shortcuts as normal.
Good boundaries create friction against those shortcuts.
That friction should not be excessive. If every simple change requires ceremony, the architecture becomes heavy. But a small amount of friction in the right places is useful. It makes developers pause before crossing a boundary and ask whether the dependency or responsibility really belongs there.
A helpful test is:
Does this boundary make the intended dependency direction easier than the unintended one?
If application services naturally depend on repository abstractions, but repositories cannot easily depend on UI or application-specific concerns, the direction is clear. If domain objects can express rules without knowing storage details, the boundary is helping. If infrastructure details are hidden behind focused abstractions, higher layers are less likely to be shaped by technical mechanics.
Boundaries are most effective when they combine structure with language. A project reference can prevent some dependencies, but naming and API design often do just as much work. A method named GetPublishedArticlesAsync guides callers differently than GetAllAsync. A policy named ArticlePublishingPolicy makes a business decision visible. A command handler named PublishArticleHandler suggests orchestration rather than general-purpose article manipulation.
In that sense, clear boundaries are not only restrictions. They are signals.
They tell developers where decisions belong, which details should remain hidden, and which direction knowledge should flow. When those signals are consistent, the architecture starts helping the team make better decisions before code review is needed.
5. Naming, APIs, and Discoverability
Architecture guides developers partly through structure, but also through language. The names of types, methods, parameters, and results tell developers what the system expects from them. When those names are precise, the intended use becomes easier to discover. When they are vague, the architecture leaves too much open to interpretation.
A class named ArticleService can mean almost anything. It may contain publishing rules, search behavior, formatting logic, persistence coordination, validation, notification handling, or UI-specific decisions. The name does not strongly guide responsibility.
A more specific name narrows the possible meaning:
ArticlePublishingPolicy
ArticleSearchService
ArticleSummaryFormatter
PublishArticleHandler
ArticleRepository
These names are not automatically better in every context, but they give stronger signals. They help a developer understand what kind of logic belongs there and what kind probably does not.
APIs have the same effect. A vague method often pushes responsibility outward:
var articles = await _articleRepository.GetAllAsync(ct);
The caller must then decide what to do with the result. Should unpublished articles be filtered out? Should ordering be applied? Should future-dated articles be excluded? Should archived articles be hidden? If several callers make those decisions independently, the system becomes inconsistent.
A more intentional API communicates the use case directly:
var articles = await _articleRepository.GetPublishedArticlesAsync(ct);
or:
var articles =
await _articleSearchService.SearchPublishedArticlesAsync(query, ct);
The difference is not only naming style. The API changes what the caller needs to know. It makes the intended operation discoverable and reduces the temptation to recreate the rule elsewhere.
Good APIs also make invalid or inappropriate use harder to express. If callers do not receive raw persistence details, they are less likely to shape database behavior outside the repository. If a domain object exposes behavior such as Publish, Archive, or UpdateContent, callers are less likely to manipulate state directly. If a result type expresses success, failure, and reason clearly, callers are less likely to infer behavior from nulls, exceptions, or loosely related flags.
For example, this is easy to misuse:
article.IsPublished = true;
article.PublishedAtUtc = now;
A more expressive operation provides better guidance:
article.Publish(now);
The method name tells the caller what is happening, and the method itself can protect the rules that make publishing valid.
Discoverability matters because developers should not need to read the entire codebase before making a small change. A well-designed architecture helps them find the right place by following the language of the system. If they need to publish an article, there should be something named around publishing. If they need to decide whether publishing is allowed, there should be a policy, method, or result that makes that decision visible.
This is also why overly generic names weaken architecture. Words like Manager, Helper, Processor, Utility, and Handler can be useful in specific contexts, but they often become vague containers. If a name does not make responsibility clearer, it may invite unrelated behavior.
A useful test is:
Would a developer unfamiliar with this feature know where to look first?
If the answer is yes, the architecture is helping. If the answer is no, the problem may not be the developer’s lack of knowledge. The problem may be that the system has not made its responsibilities discoverable enough.
6. Making Invalid Paths Inconvenient
Good architecture should not only make the intended path visible. It should also make invalid or harmful paths inconvenient enough that developers notice they are leaving the design.
This does not mean making the system difficult to use. It means avoiding APIs and structures that make poor choices feel like the natural shortcut.
For example, if article publishing has rules, the system should not encourage callers to publish by setting properties directly:
article.IsPublished = true;
article.PublishedAtUtc = now;
That path is too easy. It allows callers to bypass validation, workflow rules, event handling, and any future behavior that should happen when an article is published.
A better design makes the intended operation explicit:
article.Publish(now);
Now the caller does not need to know every rule involved in publishing. The operation has a clear home, and the object can protect its own state.
The same principle applies to persistence. If most callers should not retrieve all records and apply their own visibility rules, the architecture should not make GetAllAsync the most convenient choice for every use case. More focused methods or query objects can make the intended path easier:
await _articleRepository.GetPublishedArticlesAsync(ct);
or:
await _articleRepository.SearchAsync(query, ct);
This does not prevent all misuse, but it changes what feels natural. The developer is guided toward expressing intent instead of reconstructing rules manually.
Invalid paths can also be made inconvenient through access control. Some setters can be private. Constructors can require valid values. Value objects can prevent invalid states. Infrastructure types can be kept out of higher layers. Interfaces can expose only the operations callers are meant to use.
Invalid paths can also be made inconvenient through encapsulation and controlled mutation.
public string Title { get; private set; }
public void UpdateTitle(string title)
{
var normalizedTitle = title.Trim();
if (string.IsNullOrWhiteSpace(normalizedTitle))
{
throw new InvalidArticleStateException("Title is required.");
}
Title = normalizedTitle;
}
This design does not rely only on developer discipline. It makes direct invalid mutation unavailable and gives the rule a clear place to live.
There is an important balance here. If the valid path becomes too ceremonial, developers will look for shortcuts. Architecture that creates excessive friction often trains people to bypass it. The goal is therefore not maximum restriction, but useful guidance.
The intended path should usually be:
- easy to find,
- easy to call,
- named in the language of the system,
- hard to accidentally misuse,
- and located where the responsibility belongs. The invalid path should require enough effort that the developer has time to notice the design smell.
A useful question is:
Is the easiest way to write this code also the healthiest way?
If the answer is often no, the architecture may be guiding developers in the wrong direction.
7. The Balance Between Guidance and Friction
Architecture should guide developers, but it should not constantly get in their way.
A system that provides no guidance leaves too many decisions open. Developers must rely on memory, convention, or code review to know where behavior belongs. Over time, similar logic may appear in different places, boundaries become weaker, and the design becomes harder to explain.
But a system with too much friction creates a different problem. Every small change requires several abstractions, factories, registrations, mappings, wrappers, and indirect calls. The architecture may be technically consistent, but ordinary development becomes unnecessarily heavy. When that happens, developers begin looking for shortcuts, and those shortcuts often weaken the design even more.
Good architecture sits between those extremes.
It should make the intended path clear without making it tedious. It should protect important boundaries without requiring ceremony for every small decision. It should make misuse visible without making normal use difficult.
For example, a domain object should protect its important invariants, but it should not require a complex workflow for a simple state change. A repository should hide persistence details, but it should not require callers to construct overly complex query objects for ordinary retrieval. An application service should coordinate use cases, but it should not become a pass-through layer that adds no value.
Friction is useful when it prevents real damage. It is useful when it stops infrastructure details from leaking upward, prevents invalid state changes, protects business rules, or keeps ownership clear. Friction is harmful when it only exists to satisfy a pattern, preserve speculative flexibility, or make the architecture look more formal than the problem requires.
A useful test is:
Does this extra step protect an important decision, or does it only prove that the architecture exists?
If the extra step protects responsibility, consistency, or a meaningful boundary, it may be valuable. If it only adds ceremony, it may be architectural noise.
The best guidance often feels almost invisible. Developers do not feel forced into the right design; the right design simply appears to be the most natural path. Names point to the correct responsibility. APIs express intent. Boundaries hide irrelevant details. Invalid paths are inconvenient, but ordinary work remains straightforward.
That is the balance to aim for: enough structure to prevent drift, but not so much that the structure becomes the main thing developers have to think about.
This is closely related to the dangers of unnecessary indirection discussed in When Abstractions Become Noise.1
8. Practical Signs that the Architecture Is Helping
Architecture is often evaluated through diagrams, layering rules, patterns, or dependency direction. Those things matter, but they do not always reveal whether the system is genuinely helping developers make coherent decisions during everyday work.
A more useful measure is often behavioral.
When the architecture is helping, certain patterns begin appearing naturally across the system without requiring constant correction. Developers start placing similar responsibilities in similar places. New features resemble existing features structurally. Business rules become easier to locate. Changes feel more predictable.
One practical sign is that developers can often guess where functionality belongs before searching extensively through the solution.
For example:
- Publishing rules are expected near publishing behavior.
- Persistence concerns are expected near repositories or infrastructure.
- Application workflows are expected in application services or handlers.
- Presentation formatting is expected near the UI layer.
- Validation rules appear consistently rather than randomly. The architecture creates expectations that are usually correct.
Another sign is that code review discussions shift away from structural confusion and toward actual business or design decisions. Instead of repeatedly asking:
- “Why is this logic in the UI?”
- “Why does this repository know about workflows?”
- “Why is this service manipulating infrastructure details?”
- “Why is this validation duplicated?” the review can focus more on correctness, clarity, and behavior.
A healthy architecture also tends to make changes more local. When responsibilities are placed coherently, modifying one concern requires fewer unrelated changes elsewhere. Adding a publishing rule should primarily affect publishing-related code. Changing persistence mechanics should not force changes across the application layer. Updating UI formatting should not require touching domain logic.
Another practical sign is that accidental misuse becomes easier to notice quickly.
For example:
- a repository suddenly depending on HTTP concerns,
- a controller shaping database queries directly,
- a domain object requiring infrastructure access,
- or business rules appearing inside view models should stand out because they do not match the surrounding design language.
This is closely related to consistency. Consistency is not valuable merely because systems look uniform. It is valuable because it strengthens expectations. When similar responsibilities are handled similarly, deviations become easier to recognize.
Good architecture also tends to reduce the amount of explanation required for ordinary development. Developers do not constantly need to ask:
- “Where should this go?”
- “Which layer owns this rule?”
- “Is this supposed to happen here?”
- “Why does this type exist?” The structure itself begins answering many of those questions.
This does not mean confusion disappears entirely. Complex systems will always involve judgment calls and tradeoffs. But a helpful architecture reduces unnecessary ambiguity. It narrows the number of plausible locations for a responsibility and makes the intended direction easier to discover.
A useful question is:
Does the architecture reduce the number of decisions developers must repeatedly rediscover?
If the answer is yes, the architecture is likely helping. If the same placement questions continue appearing throughout the life of the system, the structure may not be guiding behavior as effectively as it appears on paper.
9. Conclusion
Good architecture is not only about separating code into layers, projects, services, and abstractions. Those structures matter, but they are only useful when they help developers make better decisions as the system evolves.
A helpful architecture guides behavior.
It makes responsibilities easier to discover, boundaries easier to respect, and misplaced code easier to notice. It reduces the number of decisions developers must repeatedly rediscover and gives the system a stronger sense of direction.
This is why it is not enough for the right code merely to be possible. In a healthy system, the right code should usually feel natural. The names, APIs, dependencies, and boundaries should all point developers toward the places where behavior belongs.
At the same time, architecture should not become rigid or ceremonial. Too much friction can be just as harmful as too little guidance. If every small change requires excessive indirection, developers will either slow down unnecessarily or begin bypassing the design.
The goal is balance.
Good architecture makes the intended path visible and convenient. It makes unhealthy shortcuts feel questionable. It protects important responsibilities without turning ordinary development into ceremony.
When architecture works this way, it becomes more than a static structure. It becomes part of how the system communicates with the people maintaining it.
It helps developers see where code belongs, when a boundary is being crossed, and when a design decision deserves more thought.
That is the practical value of making wrong code feel wrong: the system becomes easier to change without constantly depending on memory, discipline, or correction after the fact.
10. References
Need a second opinion on a .NET architecture decision?
I offer practical architecture and maintainability advisory for .NET teams working on long-lived business applications.
Want occasional updates?
Subscribe to receive occasional practical notes on .NET architecture, maintainable software, project progress, and new articles.
