- Published on
A Tale of Two Responsibilities
- Authors
- Name
- Fabricio Rateni
- @FabricioRtn
TLDR
Typically, once we receive arguments from an external system, we validate them. Oftentimes, after the validation is done, developers continue using the same type of argument received from the outside. However, it is very likely that some of the properties better represent the domain if we apply transformations to them. That is, the way we gather certain data from external sources, might be slightly different from the data that we need to use to compute something, let's say, an interest calculation. For example, we might be forced to accept string properties that represent numbers, Nullable
properties that represent attributes that are actually required, etc.
Proposed approach is: once validation is done, map any external argument to some other type that represents a Valid
argument. In addition to handling transformations, a Valid
argument is now our key to be able to call the components that use the data safely. That is, if there is a Valid
argument instance, we know for sure that validation already happened.
In other words, the proposed approach uses the type system to prevent invalid operations such as calling a “worker” object with arguments that have not been validated yet.
The Problem
Time ago, my team was working on a calculation engine library. All components in the engine had two stages:
- Validation stage: to verify the calculation was feasible given certain Arguments
- Calculation stage: it would grab the Arguments and use them to compute a number.
Also, the library could be consumed from other applications. We couldn’t make any assumptions at all about how the API was going to be called. For example, pretty much all arguments accepted by our components contained Nullable
values. Also, we always wanted to give the most accurate error message. For example, if a null
value was provided for a field called Rate
, we wanted our validation message to be “Rate is null” instead of a more generic “Rate is null or zero”.
Let’s understand some of the issues we faced using an example component that calculates the Interest of a loan.
A developer familiar with the Single Responsibility Principle would likely split the mentioned stages into 2 different classes. Let’s say: Interest
and InterestValidator
. Before analyzing those, let’s see how we gather data from the outside by inspecting the InterestArgs
type.
public record InterestArgs
{
public decimal? Principal { get; init; }
public decimal? Rate { get; init; }
public int? PeriodInYears { get; init; }
}
As you can see, all fields are Nullable
types. As mentioned, we can use that fact to tell apart between library calls sending zero vs null
for each of those fields, thus, returning more accurate error messages.
Not a solution
One initial approach to our Interest
class is shown below:
public class Interest
{
private InterestValidator Validator { get; init; }
public Interest(InterestValidator validator)
{
this.Validator = validator;
}
public decimal Calculate(InterestArgs args)
{
var validationResult = this.Validator.Validate(args);
if (validationResult.Errors.Count > 0)
{
throw new ValidationException(validationResult);
}
return (args.Principal ?? 0) * (args.Rate ?? 0) * (args.PeriodInYears ?? 0) / 100;
}
}
...and its InterestValidator
companion class:
public class InterestValidator
{
public InterestValidatorResult Validate(InterestArgs rawArgs)
{
var errors = GetErrors(rawArgs);
if (errors.Any())
{
return new InterestValidatorResult(errors);
}
return new InterestValidatorResult(Enumerable.Empty<string>());
}
private static IEnumerable<string> GetErrors(InterestArgs rawArgs)
{
if (rawArgs.Principal == null)
{
yield return "Principal cannot be null";
}
else if (rawArgs.Principal <= 0)
{
yield return "Principal must be greater than zero.";
}
if (rawArgs.Rate == null)
{
yield return "Rate cannot be null";
}
else if (rawArgs.Rate <= 0)
{
yield return "Rate must be greater than zero.";
}
if (rawArgs.PeriodInYears == null)
{
yield return "PeriodInYears cannot be null";
}
else if (rawArgs.PeriodInYears <= 0)
{
yield return "PeriodInYears must be greater than zero.";
}
}
}
This works fine but it suffers from -at least- a couple of issues:
It makes too many assumptions about the input data: All nullable fields that are numeric are being defaulted to 0 which means that another developer inspecting the code may mistakenly think that the “defaulting to zero” is a business rule when it is actually a consequence of the mismatch between “how we collect the argument data” and “the data we need to compute stuff safely”.
- Of course, she could have taken a look at the
Validator
class and “learn” that for example,Principal
cannot be null. However, that adds unnecessary navigation just to understand something that could have been clear from the scratch. In more complex scenarios, it could be virtually impossible.
- Of course, she could have taken a look at the
It is not friendly for API clients that want to know whether a set of arguments is correct before making the call to
Calculate
. They would need to either add atry-catch
for theValidationException
making their code less elegant or make a call toValidator.Validate()
themselves. The latter approach might seem cleaner but now the validation logic would be executed twice, one within their code and one within ourInterest
class.
How can we change our design to get rid of the mismatch and still have a dev-friendly API ?
Types to the rescue
At the time we were developing this calculation engine, I was reading an excellent F# book by Isaac Abraham. There is a chapter that talks about “Accepting data from external systems” and how we can protect our business logic from unsafe data that comes from external sources.
The answer is fairly simple: just use 2 different types for your args
:
- one for the unsafe/yet-to-be-validated data
- one for the data once has been validated.
That is, if your application accepts an object called InterestArgs
, pass it through a Validator
method/class and have that validation logic return a new object of type ValidInterestArgs
in case all data is valid. Otherwise, return a collection of errors.
Now all the calculation methods can safely assume their working data is valid because they now accept a ValidInterestArgs
instead of InterestArgs
.

We were using C# at that time (not F#) but we thought that idea should work regardless of the language (as long as it is strongly typed).
The solution
Let's introduce a new type ValidInterestArgs
as shown below. I'll copy InterestArgs
right above it for comparison:
public record InterestArgs
{
public decimal? Principal { get; init; }
public decimal? Rate { get; init; }
public int? PeriodInYears { get; init; }
}
public record ValidInterestArgs
{
public decimal Principal { get; init; }
public decimal Rate { get; init; }
public int PeriodInYears { get; init; }
}
Not surprisingly, ValidInterestArgs
is very similar to InterestArgs
except for the fact that all its properties are non-nullable. That is, we have also applied transformations to simplify the usage.
Other types of transformations could be, for example, dropping properties that are required for the Validation stage but not for the Calculation stage.
Our new InterestValidator
looks like this:
public class InterestValidator
{
public InterestValidatorResult Validate(InterestArgs rawArgs)
{
var errors = GetErrors(rawArgs);
if (errors.Any())
{
return new InterestValidatorResult(errors);
}
var validArgs = new ValidInterestArgs()
{
Principal = rawArgs.Principal!.Value,
Rate = rawArgs.Rate!.Value,
PeriodInYears = rawArgs.PeriodInYears!.Value,
};
return new InterestValidatorResult(validArgs);
}
private static IEnumerable<string> GetErrors(InterestArgs rawArgs)
{
if (rawArgs.Principal == null)
{
yield return "Principal cannot be null";
}
else if (rawArgs.Principal <= 0)
{
yield return "Principal must be greater than zero.";
}
if (rawArgs.Rate == null)
{
yield return "Rate cannot be null";
}
else if (rawArgs.Rate <= 0)
{
yield return "Rate must be greater than zero.";
}
if (rawArgs.PeriodInYears == null)
{
yield return "PeriodInYears cannot be null";
}
else if (rawArgs.PeriodInYears <= 0)
{
yield return "PeriodInYears must be greater than zero.";
}
}
}
We are using the Nullable reference types feature available since C# 8. In our case, we need to add !
to indicate that it is safe to access the value of our Nullable fields. We know it is safe because they have been validated in GetErrors
method but the compiler does not.
var validArgs = new ValidInterestArgs()
{
Principal = rawArgs.Principal!.Value,
Rate = rawArgs.Rate!.Value,
PeriodInYears = rawArgs.PeriodInYears!.Value,
};
The return object from InterestValidator.Validate()
is a InterestValidatorResult
which is just a simple type to return either a ValidInterestArgs
or a list of error messages.
public record InterestValidatorResult
{
public ValidInterestArgs? ValidArgs { get; }
public List<string> Errors { get; }
public InterestValidatorResult(ValidInterestArgs validArgs)
{
this.ValidArgs = validArgs;
this.Errors = Enumerable.Empty<string>().ToList();
}
public InterestValidatorResult(IEnumerable<string> errors)
{
this.ValidArgs = null;
this.Errors = errors.ToList();
}
}
Unfortunately, C# does not include any built-in Either or Validation<FAIL,SUCCESS> functional-programming-like classes. We couldn't afford adding a third party library such as language-ext at that time. So we moved forward with above custom type that "simulates" an Either by using different constructors for each case.
And our Interest
class is now very straightforward:
public class Interest
{
public decimal Calculate(ValidInterestArgs args)
{
return args.Principal * args.Rate * args.PeriodInYears / 100;
}
}
Since Interest
accepts a ValidInterestArgs
, it is impossible to skip validations by mistake. The type system takes care of controlling only valid workflow. Developers are forced to call InterestValidator
which is the only class with the responsibility of creating instances of type ValidInterestArgs
.
Conclusion
Using the right types instead of trying to "reuse" existing types when they have been created with a different goal in mind can simplify our code and prevent subtle issues.
You can get take a look at the complete example here: https://github.com/fabricior/validators-example.