0

For an attribute which need to be validated, lets say for an entity we have country field as VO This country field needs to be validated to be alpha-3 code as per some business logic required by domain expert.

NOTE:

*We need to persist this country data as it can have other values also and possible in future there can be addition, updating and deleting of the country persisted data.

This is just one example using country code which may rarely change, there can be other fields which needs to be validated from persistence like validating some quantity with wrt data in persistence and it won't be efficient to store them in memory or prefetching them all.

Another valid example can be user creation with unique and valid domain email check, which will need uniqueness check from persistence *

Case 1.

Doing validation in application layer:

If we call repository countryRepo.getCountryByCountryAlpha3Code() in application layer and then if the value is correct and valid part of system we can then pass the createValidEntity() and if not then can throw the error directly in application layer use-case.

Issue:

  • This validation will be repeated in multiple use-case if same validation need to be checked in other use-cases if its application layer concern
  • Here the business logic is now a part of application service layer

Case 2

Validating the country code in its value object class or domain service in Domain Layer

Doing this will keep business logic inside domain layer and also won't violate DRY principle.

import { ValueObject } from '@shared/core/domain/ValueObject';
    import { Result } from '@shared/core/Result';
    import { Utils } from '@shared/utils/Utils';
    
    interface CountryAlpha3CodeProps {
      value: string;
    }
    
    export class CountryAlpha3Code extends ValueObject<CountryAlpha3CodeProps> {
      // Case Insensitive String. Only printable ASCII allowed. (Non-printable characters like: Carriage returns, Tabs, Line breaks, etc are not allowed)
    
      get value(): string {
        return this.props.value;
      }
    
      private constructor(props: CountryAlpha3CodeProps) {
        super(props);
      }
    
      public static create(value: string): Result<CountryAlpha3Code> {
        
        
        return Result.ok<CountryAlpha3Code>(new CountryAlpha3Code({ value: value }));
      }
    }
  • Is it good to call the repository from inside domain layer (Service or VO (not recommended) ) then dependency flow will change?

  • If we trigger event how to make it synchronous?

  • What are some better ways to solve this?



export default class UseCaseClass implements IUseCaseInterface {
  constructor(private readonly _repo: IRepo, private readonly countryCodeRepo: ICountryCodeRepo) {}

  async execute(request: dto): Promise<dtoResponse> {
    const someOtherKeyorError = KeyEntity.create(request.someOtherDtoKey);
    const countryOrError = CountryAlpha3Code.create(request.country);
    
        const dtoResult = Result.combine([
          someOtherKeyorError, countryOrError
        ]);
    
        if (dtoResult.isFailure) {
          return left(Result.fail<void>(dtoResult.error)) as dtoResponse;
        }
    
        try {
          // -> Here we are just calling the repo
           const isValidCountryCode = await this.countryCodeRepo.getCountryCodeByAlpha2Code(countryOrError.getValue()); // return boolean value
    
          if (!isValidCountryCode) {
           return left(new ValidCountryCodeError.CountryCodeNotValid(countryOrError.getValue())) as dtoResponse;
        }
    
          const dataOrError = MyEntity.create({...request,
            key: someOtherKeyorError.city.getValue(),
            country: countryOrError.getValue(),
          });
    
     
          const commandResult = await this._repo.save(dataOrError.getValue());
    
          return right(Result.ok<any>(commandResult));
        } catch (err: any) {
          return left(new AppError.UnexpectedError(err)) as dtoResponse;
        }
      }
    }

In above application layer,

this part of code :


  const isValidCountryCode = await this.countryCodeRepo.getCountryCodeByAlpha2Code(countryOrError.getValue()); // return boolean value
       
  if (!isValidCountryCode) {
   return left(new ValidCountryCodeError.CountryCodeNotValid(countryOrError.getValue())) as dtoResponse;
   }

it it right to call the countryCodeRepo and fetch result or this part should be moved to domain service and then check the validity of the countryCode VO?

UPDATE:

After exploring I found this article by Vladimir Khorikov which seems close to what I was looking, he is following

enter image description here

As per his thoughts some domain logic leakage is fine, but I feel it will still keep the value object validation in invalid state if some other use case call without knowing that persistence check is necessary for that particular VO/entity creation.

I am still confused for the right approach

Mr X
  • 1,637
  • 3
  • 29
  • 55

1 Answers1

0

In my opinion, the conversion from String to ValueObject does not belong to the Business Logic at all. The Business Logic has a public contract that is invoked from the outside (API layer or presentation layer maybe). The contract should already expect Value Objects, not raw strings. Therefore, whoever is calling the business logic has to figure out how to obtain those Value Objects.

Regarding the implementation of the Country Code value object, I would question if it is really necessary to load the country codes from the database. The list of country codes very rarely changes. The way I've solved this in the past is simply hardcoding the list of country codes inside the value object itself.

Sample code in pseudo-C#, but you should get the point:

public class CountryCode : ValueObject
{
   // Static definitions to be used in code like:
   // var myCountry = CountryCode.France;
   public static readonly CountryCode France = new CountryCode("FRA");
   public static readonly CountryCode China = new CountryCode("CHN");
   [...]

   public static AllCountries = new [] {
      France, China, ...
   }
   
   public string ThreeLetterCode { get; }
   
   private CountryCode(string threeLetterCountryCode)
   {
      ThreeLetterCode = threeLetterCountryCode;
   }

   public static CountryCode Parse(string code)
   {
      [...] handle nulls, empties, etc

      var exists = AllCountries.FirstOrDefault(c=>c.ThreeLetterCode==code);
      if(exists == null)
         // throw error

      return exists;
   }

}

Following this approach, you can make a very useful and developer-friendly CountryCode value object. In my actual solution, I had both the 2 and 3-letter codes and display names in English only for logging purposes (for presentation purposes, the presentation layer can look up the translation based on the code).

If loading the country codes from the DB is valuable for your scenario, it's still very likely that the list changes very rarely, so you could for example load a static list in the value object itself at application start up and then refresh it periodically if the application runs for very long.

Francesc Castells
  • 2,692
  • 21
  • 25
  • Country code is just one example, though it rarely change it will work storing them in memory, but for other cases it maybe a large quantity which won't be efficient to call all values, for example to measure some quantity and validating from persistence would be only option. How would that be made? Will also update question accordingly. – Mr X Jun 30 '22 at 01:31
  • @MrX OK. The CountryCode VO was just a sample based on your question, but does my first paragraph answer your question? Basically, if we are talking about user input validation, this will happen before entering the Business Layer. – Francesc Castells Jun 30 '22 at 08:55
  • Think of it as user creation with valid and unique email email, though email is still string, but for user class its EmailVO and in EmailVO, we can checked email validity but for uniqueness either we have to check the uniqueness in application layer which will impact domain completeness as some business logic is leaking and can make the user creation in invalid state with other use-case or inject some repo-domain service interface which will make domain impure, and the last one as you mentioned to pre load will impact performance. I know all three can't be achieved but what is the best one. – Mr X Jun 30 '22 at 14:46