When Validation Has No Clear Owner
Single Responsibility · 2026-05-06
A practical look at why validation often becomes scattered across UI, application, domain, and persistence layers, and how assigning each kind of validation a clear owner makes systems easier to change and reason about.
When Validation Has No Clear Owner
The question is not whether validation should exist in several places, but whether each validation rule has the right owner.
1. Introduction: Validation appears everywhere
Validation is one of those parts of a system that seems simple until the system grows.
At first, the rules may be obvious. A title is required. An email address must have a usable format. A date cannot be in the past. A quantity must be greater than zero.
These rules need to be checked somewhere, and the first place is often wherever the need appears. A check is added to the UI so the user gets immediate feedback. Another check is added to the application service to avoid processing an invalid request. A guard clause is added to the domain object to prevent an invalid state. A database constraint is added to protect stored data.
Each of these decisions may be reasonable.
The problem starts when the system no longer has a clear idea of which layer owns which kind of validation.
The same rule may appear in the UI, in the application service, in the domain model, and in the database. Sometimes that duplication is intentional. Often it is accidental. Over time, the copies can drift apart. One layer allows something that another layer rejects. One error message says the title is missing, another says the article is invalid, and a third fails with a database exception.
At that point, validation is no longer just protecting the system. It is making the system harder to understand.
The issue is not that validation exists in several places. In many systems, it should. The UI can help the user correct input early. The application layer can reject unusable requests. The domain model can protect business invariants. Persistence can enforce constraints that must never be violated.
The real question is whether each validation rule has a clear owner.
When validation ownership is unclear, rules become duplicated, error handling becomes inconsistent, and changes become risky. A small change to one rule may require edits in several places, and developers may still be unsure whether they found all of them.
Clear validation boundaries do not remove validation from the system. They make it easier to see what each layer is responsible for protecting.
2. Validation is not one thing
One reason validation becomes confusing is that the word validation is used for several different kinds of checks.
Some validation is about user experience. It helps the user correct input before submitting a form. For example, the UI may show that a required field is empty or that a value does not look like an email address.
Some validation is about request shape. It checks whether an application operation has enough usable information to run. For example, an application service may reject a command with a missing identifier, an invalid date range, or a value that cannot be interpreted.
Some validation is about business rules. It protects what the system considers valid behavior. For example, an article may not be publishable unless it has a title and content, or an order may not be completed unless it has at least one order line.
Some validation is about persistence. It protects the data store from impossible or unsafe data. For example, a database may enforce non-null columns, unique keys, foreign keys, and length limits.
These are all forms of validation, but they are not the same responsibility.
A UI check that helps the user fill in a form is not the same as a domain rule that protects a business invariant. A database constraint that prevents corrupt stored data is not the same as an application-level check that decides whether a use case can begin.
When these distinctions are ignored, validation often gets placed wherever it is most convenient at the moment.
That can lead to two opposite problems.
The first problem is under-protection. A rule exists only in the UI, so the system behaves correctly when the user clicks through the intended screen, but not when the same operation is called from somewhere else.
The second problem is over-duplication. The same rule is copied into every layer, even when only one layer should own the decision. This makes the system harder to change because every copy has to be found and updated.
A clearer approach is to ask what kind of validation is being performed:
- Is this check helping the user enter data correctly?
- Is this check making sure the request can be processed?
- Is this check protecting a business rule or invariant?
- Is this check protecting stored data from corruption?
- Is this check really a validation rule, or is it a query, authorization, or workflow decision?
The answer usually points to the layer that should own the rule.
This does not mean a rule can never appear in more than one place. A required title may be reflected in the UI, checked in an application request, protected by the domain model, and constrained in persistence.
But those checks do not all mean the same thing.
The UI check is guidance. The application check is request protection. The domain check is invariant protection. The persistence check is a final guard for stored data.
The same visible rule can therefore have several technical expressions, but each expression should have a clear reason for existing.
3. What the UI can validate
The UI is often the best place to give immediate feedback.
If the user leaves a required field empty, enters text in the wrong format, or selects a value that clearly cannot be accepted, the UI should usually help the user correct it before the request is submitted.
That kind of validation is useful because it improves the experience of using the system. The user does not have to submit a form, wait for a response, and then discover that a simple field was missing.
For example, the UI may check:
- Whether a required field has been filled in.
- Whether a value has the expected basic format.
- Whether a number is within a visible input range.
- Whether two fields appear to match, such as repeated email addresses.
- Whether a date field contains a usable date.
- Whether the current screen has enough input to enable a button.
These checks are appropriate in the UI because they are close to the user interaction.
But UI validation should usually be treated as guidance, not authority.
A disabled button may prevent one user path, but it does not protect the system by itself. The same operation might be called from another screen, an API endpoint, a background job, an import process, or a test. If the rule matters to the correctness of the system, it needs to be enforced somewhere other than the UI.
For example, this can be useful UI behavior:
SaveButton.IsEnabled =
!string.IsNullOrWhiteSpace(ViewModel.Title) &&
!string.IsNullOrWhiteSpace(ViewModel.ContentMarkdown);
It gives the user quick feedback that the article is not ready to save.
But the system should not rely on that button state as the only protection. If an article must always have a title, that rule should also be enforced by the application layer, the domain model, or both, depending on what kind of rule it is.
A useful distinction is:
UI validation helps the user avoid mistakes. It should not be the only thing that prevents invalid system behavior.
The UI can also display validation results returned by the application layer. This is often preferable for rules that depend on business behavior, permissions, existing data, or workflow state.
For example, instead of duplicating a rule in the UI, the application layer might return:
public sealed class ArticleEditorViewModel
{
public required string Title { get; init; }
public required string ContentMarkdown { get; init; }
public required bool CanSave { get; init; }
public required IReadOnlyList<string> ValidationMessages
{
get; init;
}
}
The UI can then decide how to present those messages and whether to enable or disable actions, without becoming the owner of the rules.
That keeps the UI focused on interaction:
- Show the problem clearly.
- Help the user correct it.
- Prevent obviously incomplete actions when possible.
- Submit the request to the application layer.
- Display the result in a useful form.
This is a good responsibility for the UI.
The problem begins when the UI becomes the only place where important validation rules exist.
4. What the application layer should validate
The application layer should usually validate whether a request is usable for the use case it represents.
This is different from UI validation. The UI may help the user fill in a form correctly, but the application layer should not assume that every request came from that UI. A use case may be called from a web page, an API endpoint, a background process, an import routine, a test, or another part of the application.
The application layer therefore needs to protect the use case boundary.
For example, an application service or command handler may check:
- Whether a required identifier has been provided.
- Whether the current user or caller is allowed to perform the operation.
- Whether the request contains enough information to continue.
- Whether a referenced object exists.
- Whether a requested operation makes sense in the current application workflow.
- Whether the request can be translated into a valid domain operation.
These checks are not mainly about presentation. They are about deciding whether the application can start or complete the use case.
For example:
public async Task<UpdateArticleResult> UpdateArticleAsync(
UpdateArticleRequest request,
CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.ArticleId))
{
return UpdateArticleResult.Failed("Article id is required.");
}
var article = await _articleRepository.GetByIdAsync(
request.ArticleId,
ct);
if (article is null)
{
return UpdateArticleResult.Failed("Article was not found.");
}
article.UpdateContent(
request.Title,
request.Summary,
request.ContentMarkdown ?? string.Empty,
_clock.UtcNow);
await _articleRepository.SaveAsync(article, ct);
return UpdateArticleResult.Success(article.Id);
}
In this example, the application layer checks that the request identifies an article and that the article exists. Those are use case boundary checks.
But the application layer should be careful not to become the owner of every rule.
If the rule is about whether an article can exist in a certain state, it probably belongs in the domain model. If the rule is about which records should be retrieved, it may belong in the repository. If the rule is about how an error should be shown to the user, it belongs in the UI.
A common problem appears when application services start collecting all validation rules because they are close to the use case.
For example:
if (string.IsNullOrWhiteSpace(request.Title))
{
return UpdateArticleResult.Failed("Title is required.");
}
if (request.Title.Length > 120)
{
return UpdateArticleResult.Failed("Title is too long.");
}
if (string.IsNullOrWhiteSpace(request.ContentMarkdown))
{
return UpdateArticleResult.Failed("Content is required.");
}
These checks may be appropriate in the application layer if they are only request-shape rules. But if they define what makes an article valid, they should probably be owned by the article itself or by a domain policy.
The application layer can still translate domain failures into application-level results:
try
{
article.UpdateContent(
request.Title,
request.Summary,
request.ContentMarkdown ?? string.Empty,
_clock.UtcNow);
await _articleRepository.SaveAsync(article, ct);
return UpdateArticleResult.Success(article.Id);
}
catch (InvalidArticleStateException ex)
{
return UpdateArticleResult.Failed(ex.Message);
}
This keeps the application service responsible for orchestration and result handling, while the domain model remains responsible for protecting valid article state.
A useful distinction is:
The application layer should validate whether the use case can proceed. It should not automatically become the owner of every rule the use case depends on.
That boundary keeps application services focused. They can reject unusable requests, coordinate the operation, translate results, and handle application-level concerns without becoming a container for all validation logic in the system.
5. What the domain model must protect
The domain model should protect the rules that define valid business state and behavior.
This is different from checking whether a request is well-formed. It is also different from helping the user fill in a form. The domain model is responsible for making sure that the important concepts in the system cannot easily be placed into a state that does not make sense.
For example, an article may require a title before it can be published. An order may require at least one order line before it can be completed. A subscription may not be renewable after it has been cancelled. These are not just input checks. They are rules about what the system means.
Those rules should not depend on which UI screen, API endpoint, or background job triggered the operation.
If an article cannot be published without content, that should be true regardless of whether the publish operation was started from an admin page, a batch process, an import tool, or a test.
That is why the domain model should protect business invariants.
An invariant is a rule that must remain true for the object or concept to be valid. If the invariant is broken, the system is no longer only dealing with invalid input. It is dealing with invalid state.
For example:
public void Publish(DateTime publishedUtc)
{
if (string.IsNullOrWhiteSpace(Title))
{
throw new InvalidArticleStateException(
"An article must have a title before it can be published.");
}
if (string.IsNullOrWhiteSpace(ContentMarkdown))
{
throw new InvalidArticleStateException(
"An article must have content before it can be published.");
}
IsPublished = true;
PublishedUtc = publishedUtc;
}
In this example, the rule belongs naturally with the article. The application service may decide when to ask the article to publish itself, but the article protects what it means to be publishable.
That is a better boundary than this:
if (!string.IsNullOrWhiteSpace(article.Title) &&
!string.IsNullOrWhiteSpace(article.ContentMarkdown))
{
article.IsPublished = true;
article.PublishedUtc = _clock.UtcNow;
}
The second version may work in one use case, but the rule is now outside the article. If another part of the system publishes an article, the same rule must be remembered and repeated. If the rule changes, every copy must be found.
The domain model does not need to own every validation rule. It should not normally care whether a form field was left empty, whether a JSON request had the right shape, or how a validation message is displayed to the user.
But it should protect the rules that make the business object valid.
For example, the domain model may enforce:
- Required state for important operations.
- Valid transitions between states.
- Rules that must apply no matter who calls the operation.
- Relationships between values that must stay consistent.
- Business limits that are part of the concept itself.
- Operations that should be impossible when the object is in the wrong state.
This is especially important because domain objects are often used from more than one place. A rule placed only in the UI protects one screen. A rule placed only in an application service protects one use case. A rule placed in the domain model protects the concept wherever it is used.
That does not mean domain objects should become large containers for every possible rule. Some rules depend on external services, current user permissions, configuration, or workflow policies. Those may belong in domain services, policies, or the application layer.
For example, whether an article has enough content to be published may belong in the Article object. Whether the current user is allowed to publish it may belong in an authorization policy or application-level check.
A useful distinction is:
The domain model should protect what must always be true for the business concept, not every condition that happens to be checked during a use case.
When this boundary is clear, the domain model becomes more than a data structure. It becomes the place where core business meaning is protected.
That makes the rest of the system safer. The UI can guide the user. The application layer can coordinate the use case. Persistence can protect stored data. But the domain model remains responsible for preventing invalid business state from being created in the first place.
6. What persistence constraints are still useful for
Persistence constraints are still important, even when validation has clear ownership elsewhere.
A database should not be treated as the main place where business meaning is expressed, but it should still protect the integrity of stored data. Non-null constraints, unique indexes, foreign keys, length limits, and required relationships can prevent invalid or inconsistent data from being persisted.
For example, if an article title is required, the domain model may own the business rule that an article must have a title. The application layer may translate that rule into a useful result. The UI may help the user correct the problem early.
But the database can still have a non-null constraint on the title column.
That does not mean the database owns the rule. It means the database provides a final guard.
The same applies to uniqueness. If article slugs must be unique, the application layer may check whether a slug is available before saving. But the database should still enforce uniqueness, because two requests may arrive at nearly the same time.
For example:
if (await _articleRepository.SlugExistsAsync(request.Slug, ct))
{
return CreateArticleResult.Failed("The slug is already in use.");
}
This check can give a useful application-level result, but it should not be the only protection if uniqueness really matters. A unique index in the database is still appropriate.
Persistence constraints are especially useful for protecting against:
- Missing required values.
- Broken relationships.
- Duplicate values that must be unique.
- Values that exceed storage limits.
- Data corruption caused by bugs, imports, migrations, or concurrent operations.
The important distinction is that persistence constraints should usually protect storage integrity. They should not become the normal place where business rules are discovered.
If the first sign of an invalid article is a database exception, the rule may be enforced too late. The user experience will usually be worse, and the application code may become harder to reason about.
A useful distinction is:
Persistence constraints are final protection for stored data, not a replacement for clear validation ownership.
When persistence constraints are used this way, they support the rest of the architecture instead of replacing it.
7. Example: duplicated validation across layers
Consider an article editor where an article must have a title before it can be saved or published.
The rule may start in the UI:
SaveButton.IsEnabled = !string.IsNullOrWhiteSpace(ViewModel.Title);
This is reasonable. It gives the user immediate feedback and prevents an obviously incomplete action from the current screen.
Later, the same rule is added to the application service:
if (string.IsNullOrWhiteSpace(request.Title))
{
return UpdateArticleResult.Failed("Title is required.");
}
That may also be reasonable. The application layer should not assume that every request came from the UI.
Then the rule is added to the domain object:
public void UpdateTitle(string title)
{
if (string.IsNullOrWhiteSpace(title))
{
throw new InvalidArticleStateException("Title is required.");
}
Title = title.Trim();
}
Again, this may be reasonable. If an article cannot be valid without a title, the article should protect that rule.
Finally, the database also has a non-null constraint on the title column.
At first glance, this looks like unnecessary duplication. The same rule seems to exist in four places.
But the important question is not simply whether the same condition appears more than once. The important question is whether each occurrence has a clear purpose.
In this example, each layer may be doing something different:
- The UI check guides the user before the request is submitted.
- The application check protects the use case boundary.
- The domain check protects valid article state.
- The database constraint protects stored data.
That kind of duplication can be acceptable when it is deliberate.
The problem appears when these checks are added without clear ownership and then start to drift apart.
For example, the UI might allow a title with 150 characters:
TitleTextBox.MaxLength = 150;
The application service might reject anything longer than 120 characters:
if (request.Title.Length > 120)
{
return UpdateArticleResult.Failed("Title is too long.");
}
The domain object might use a different rule:
if (title.Trim().Length > 100)
{
throw new InvalidArticleStateException(
"The article title is too long.");
}
And the database column might allow 200 characters.
Now the system has four different versions of what appears to be the same rule.
That creates several problems.
The user may be allowed to enter a value that the application rejects. The application may accept a value that the domain rejects. The domain may accept a value that the database cannot store. Error messages may differ depending on which layer rejects the value first.
The system still has validation, but it no longer has one clear understanding of the rule.
This is where duplicated validation becomes dangerous.
A better design is to decide which layer owns the rule, and then let other layers reflect or protect around that ownership.
If the maximum title length is part of what an article is, the domain model should probably own it:
public const int MaxTitleLength = 120;
public void UpdateTitle(string title)
{
var normalizedTitle = title.Trim();
if (string.IsNullOrWhiteSpace(normalizedTitle))
{
throw new InvalidArticleStateException("Title is required.");
}
if (normalizedTitle.Length > MaxTitleLength)
{
throw new InvalidArticleStateException(
$"Title cannot be longer than {MaxTitleLength} characters.");
}
Title = normalizedTitle;
}
The application layer can then call the domain operation and translate the result:
try
{
article.UpdateTitle(request.Title);
await _articleRepository.SaveAsync(article, ct);
return UpdateArticleResult.Success(article.Id);
}
catch (InvalidArticleStateException ex)
{
return UpdateArticleResult.Failed(ex.Message);
}
The UI can still help the user by showing the same limit:
TitleTextBox.MaxLength = ArticleEditorViewModel.MaxTitleLength;
or by using a value returned from the application layer:
TitleTextBox.MaxLength = ViewModel.MaxTitleLength;
The database can still enforce a compatible column length.
The difference is that the rule now has a clear owner.
Other layers may express the rule for user experience, request protection, or storage safety, but they should not independently define competing versions of it.
A useful test is:
If this rule changes, where should the change begin?
If the answer is unclear, the validation ownership is probably unclear as well.
8. Moving validation to clearer homes
Improving validation boundaries does not mean removing validation from most of the system and keeping it in only one place.
That is usually too simple.
Validation often needs to appear in several places, but each place should have a clear reason for performing the check.
The goal is not to eliminate every repeated condition. The goal is to remove unclear ownership.
A useful first question is:
What is this validation protecting?
If the check is protecting the user experience, it may belong in the UI.
If the check is protecting the use case boundary, it may belong in the application layer.
If the check is protecting valid business state, it may belong in the domain model.
If the check is protecting stored data, it may belong in persistence.
For example, consider a title length rule.
If the UI limits the title textbox to 120 characters, that helps the user avoid entering something that cannot be accepted. But the UI should not be the authority on the rule.
If the application layer rejects a request with a title that is too long, that protects the use case boundary. But it may still not be the best owner of the rule if the length limit is part of what makes an article valid.
If the Article object enforces the maximum title length, that protects the business concept itself.
If the database column is also limited to the same length, that protects the stored data.
Those checks can coexist, but the rule should still have one primary owner.
A clearer design might expose the rule from the owner rather than redefining it independently in each layer:
public sealed class Article
{
public const int MaxTitleLength = 120;
public string Title { get; private set; } = string.Empty;
public void UpdateTitle(string title)
{
var normalizedTitle = title.Trim();
if (string.IsNullOrWhiteSpace(normalizedTitle))
{
throw new InvalidArticleStateException("Title is required.");
}
if (normalizedTitle.Length > MaxTitleLength)
{
throw new InvalidArticleStateException(
$"Title cannot be longer than {MaxTitleLength} characters.");
}
Title = normalizedTitle;
}
}
The application layer can then include that limit in the model it returns to the UI:
public sealed class ArticleEditorViewModel
{
public required string Title { get; init; }
public required int MaxTitleLength { get; init; }
public required IReadOnlyList<string> ValidationMessages
{ get; init; }
}
The UI can use the value without owning it:
TitleTextBox.MaxLength = ViewModel.MaxTitleLength;
In this version, the UI still helps the user. The application layer still shapes the information needed by the screen. The domain model still protects the rule. Persistence can still enforce compatible storage constraints.
But the rule no longer has four independent definitions.
Another useful question is:
If this rule changes, which code should I expect to modify first?
If the answer is the UI, then the rule may be primarily a presentation rule.
If the answer is the application service, then the rule may be about whether a use case can proceed.
If the answer is the domain model, then the rule may be part of the business concept.
If the answer is the database schema, then the rule may be a storage constraint.
The problem is not that more than one layer might need adjustment after the rule changes. That can happen. The problem is when no layer is clearly responsible for deciding what the rule is.
Validation can also be moved gradually.
Start with one rule that currently appears in several places. Then decide which occurrence is authoritative and which occurrences are only supporting it.
For example:
- Keep UI checks that improve feedback.
- Keep application checks that protect the use case boundary.
- Move business invariants into the domain model.
- Keep database constraints that protect stored data.
- Remove copied rules that no longer have a clear purpose.
- Make supporting layers consume rule values or results from the owning layer when possible.
This does not make the system perfect, and it does not remove all judgment.
But it makes future changes safer.
When validation has clearer homes, a developer can look at a rule and understand why it exists there. That is more valuable than simply making the code shorter.
9. Relation to SOLID and common patterns
Unclear validation ownership is closely related to the Single Responsibility Principle.
A class or layer should not change for every kind of validation change in the system. If a UI component changes because a field layout changed, because a business invariant changed, because a workflow rule changed, and because a database constraint changed, it has too many reasons to change.
The same can happen in application services. If every validation rule is placed there, the service may start as a use case coordinator but gradually become the place where all input checks, business rules, authorization decisions, and persistence assumptions are collected.
That is a sign of unclear responsibility.
The Dependency Inversion Principle is also relevant. Higher-level behavior should not depend directly on technical validation details from lower-level infrastructure. For example, an application service should not rely on a database exception as the normal way to discover that an article title is invalid.
Persistence constraints are useful, but they should usually be a final guard, not the primary expression of business meaning.
Common patterns can help when they make validation ownership clearer.
For example, a Value Object can protect rules for a specific value:
public sealed class ArticleTitle
{
public const int MaxLength = 120;
public ArticleTitle(string value)
{
var normalizedValue = value.Trim();
if (string.IsNullOrWhiteSpace(normalizedValue))
{
throw new InvalidArticleStateException("Title is required.");
}
if (normalizedValue.Length > MaxLength)
{
throw new InvalidArticleStateException(
$"Title cannot be longer than {MaxLength} characters.");
}
Value = normalizedValue;
}
public string Value { get; }
}
This can be clearer than repeating title validation in every service, form, or handler that works with article titles.
A Policy or Specification can be useful when a rule is more complex or varies independently from the object itself.
For example, publishing rules might be placed behind a policy:
public interface IArticlePublishingPolicy
{
bool CanPublish(Article article);
}
This can be useful when the rule depends on workflow, configuration, user role, or other conditions that should not be hard-coded directly into the UI or application service.
A Command Handler can help protect the use case boundary:
public async Task<UpdateArticleResult> HandleAsync(
UpdateArticleCommand command,
CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(command.ArticleId))
{
return UpdateArticleResult.Failed("Article id is required.");
}
var article = await _articleRepository.GetByIdAsync(
command.ArticleId,
ct);
if (article is null)
{
return UpdateArticleResult.Failed("Article was not found.");
}
article.UpdateTitle(new ArticleTitle(command.Title));
await _articleRepository.SaveAsync(article, ct);
return UpdateArticleResult.Success(article.Id);
}
In this example, the handler checks whether the command can be processed, while the ArticleTitle value object protects the title rule itself.
Validation libraries can also be useful, especially for request-shape validation. They can make application input checks more consistent and easier to test.
But a validation library does not decide where a rule belongs.
It is possible to use a validation framework well and still put the wrong rules in the wrong layer. A framework can organize validation code, but it cannot replace architectural judgment.
The same applies to attributes on DTOs or view models. They may be convenient for simple request checks, but they should not become the only place where important business rules are expressed.
Patterns are useful when they clarify responsibility:
- Value Objects can protect valid values.
- Domain Entities can protect business state and behavior.
- Policies and Specifications can isolate rules that vary independently.
- Command Handlers can protect use case boundaries.
- View Models can expose validation state to the UI.
- Database constraints can protect stored data.
The pattern is not the important part by itself.
The important part is that each validation rule has a clear reason to live where it lives.
10. Warning signs of unclear validation ownership
Validation ownership may be unclear if the same rule appears in several places, but no one can easily say which version is authoritative.
Some duplication is normal. A rule may be reflected in the UI, checked at the application boundary, protected by the domain model, and reinforced by persistence. That is not automatically a problem.
The warning sign is when those checks no longer have distinct purposes.
For example, unclear validation ownership may show up as:
- The same rule is implemented in the UI, application service, domain model, and database, but with slightly different limits.
- Different screens show different validation messages for the same rule.
- A rule is enforced only by a disabled button or hidden menu item.
- Application services contain long blocks of validation that mix request checks, business rules, authorization, and persistence assumptions.
- Domain objects can be placed into invalid states unless the caller remembers to validate first.
- Database exceptions are used as the normal way to detect business-rule violations.
- UI tests are required to verify rules that should be testable without the UI.
- A small validation change requires edits in many unrelated files.
- Developers are unsure whether a new rule belongs in the UI, application layer, domain model, repository, or database.
- The system has several versions of the same rule, but no clear source of truth.
These signs do not all mean the same thing.
A validation rule repeated in several layers may be acceptable if each layer has a clear responsibility. A required field shown in the UI, checked in the application layer, protected by the domain model, and constrained in the database can be reasonable.
But if each layer independently defines what “required” means, the system is likely to drift.
Another warning sign is vague error handling.
For example, the UI might show:
Title is required.
The application service might return:
Article is invalid.
The domain model might throw:
Invalid article state.
And the database might fail with a technical constraint error.
Each message may be technically correct, but the user and developer experience becomes inconsistent. The system does not appear to have one clear understanding of the rule.
Unclear ownership also tends to make validation changes feel risky.
If the maximum title length changes from 120 to 150 characters, the change should have an obvious starting point. Other layers may need to reflect the new value, but there should be one place that owns the decision.
If changing the rule means searching the whole solution and hoping every copy is found, the validation boundary is probably unclear.
A useful question is:
Which part of the system would be wrong if this validation rule changed there and nowhere else?
If the answer is not obvious, the rule probably does not have a clear owner.
The goal is not to remove every repeated check.
The goal is to make sure each check has a reason to exist, and that the source of the rule is clear.
11. Improving validation boundaries gradually
Validation problems usually develop one small rule at a time, so they should usually be improved one rule at a time as well.
A large rewrite is rarely the safest first step. Validation is often tied to user experience, application flow, business behavior, and persistence. Moving too much at once can easily change behavior unintentionally.
A safer approach is to start with one rule that is already causing friction.
For example, choose a rule that appears in several places, has recently changed, has inconsistent error messages, or is difficult to test. Then ask where the rule should be owned.
A useful sequence is:
- Identify the rule.
- Find every place where the rule is currently checked.
- Decide which layer should own the rule.
- Decide which other layers should only reflect, support, or protect around the rule.
- Add or adjust tests around the owning layer.
- Update supporting layers to consume the rule or result where possible.
- Remove duplicated checks that no longer have a clear purpose.
For example, suppose an article title length rule appears in the UI, the application service, the domain model, and the database.
The first step is not necessarily to delete three of those checks. The first step is to decide which one defines the rule.
If the maximum title length is part of what an article is, the domain model may be the right owner. The domain object or value object can define and enforce the limit. The application layer can translate domain failures into application results. The UI can display the limit and guide the user. The database can keep a compatible constraint.
That gives each layer a reason to participate without giving every layer independent ownership.
A small improvement might be to introduce a shared value from the owning layer:
public sealed class ArticleTitle
{
public const int MaxLength = 120;
public ArticleTitle(string value)
{
var normalizedValue = value.Trim();
if (string.IsNullOrWhiteSpace(normalizedValue))
{
throw new InvalidArticleStateException("Title is required.");
}
if (normalizedValue.Length > MaxLength)
{
throw new InvalidArticleStateException(
$"Title cannot be longer than {MaxLength} characters.");
}
Value = normalizedValue;
}
public string Value { get; }
}
The application layer can then expose that value to the UI:
public sealed class ArticleEditorViewModel
{
public required string Title { get; init; }
public int MaxTitleLength { get; init; } = ArticleTitle.MaxLength;
public required IReadOnlyList<string> ValidationMessages
{ get; init; }
}
The UI can use it without redefining it:
TitleTextBox.MaxLength = ViewModel.MaxTitleLength;
This is a small change, but it makes the ownership clearer.
Another gradual improvement is to move only one kind of validation at a time.
For example:
- Move business invariants into the domain model.
- Move request-shape validation into command or request validators.
- Move workflow-dependent rules into policies.
- Move display-specific validation messages into the UI.
- Keep database constraints as final protection for stored data.
Trying to solve all of these at once can create unnecessary disruption. Improving one rule or one category of rules at a time is usually safer.
Tests are important during this process.
Before moving a rule, add tests that describe the behavior that must remain true. If a title is required, test that the owning layer rejects a missing title. If an article cannot be published without content, test that the domain operation prevents it. If an application request without an identifier should fail, test that the application layer rejects it.
The goal is not only to move code.
The goal is to make the rule easier to find, easier to test, and easier to change.
A useful question after each improvement is:
If this rule changes later, will the next developer know where to start?
If the answer is yes, the validation boundary has become clearer.
That is usually a better measure of progress than whether the total number of validation checks has been reduced.
12. Conclusion
Validation is necessary, but it becomes difficult when the system has no clear idea of what each layer is protecting.
The UI can guide the user. The application layer can protect the use case boundary. The domain model can protect business invariants. Persistence can protect stored data.
Those responsibilities can overlap, but they should not be confused.
A validation rule may appear in more than one place, and that is not automatically a problem. The problem is when each layer independently defines the rule, or when no one can say which version is authoritative.
That is when validation becomes harder to change, harder to test, and harder to explain.
Clear validation ownership does not mean forcing every rule into one layer. It means understanding what kind of rule it is, what it protects, and where the decision should begin when the rule changes.
When validation has a clear owner, supporting layers can still help. The UI can give immediate feedback. The application layer can return useful results. The domain model can prevent invalid state. The database can provide final protection.
But the system no longer depends on scattered, competing versions of the same decision.
That makes validation less mysterious and less fragile.
More importantly, it makes the system easier to reason about when the rules inevitably change.
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.