Back-end10 minute read

Context Validation in Domain-Driven Design

Handling all validation in domain objects results in objects that are huge and complex to work with. In domain-driven design, using decoupled validator components allows your code to be much more reusable and enables validation rules to rapidly grow.

In this article, Toptal engineer Josip Medic shows us how validation can be decoupled from domain objects, made context-specific, and structured well to achieve more sustainable validation code.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Handling all validation in domain objects results in objects that are huge and complex to work with. In domain-driven design, using decoupled validator components allows your code to be much more reusable and enables validation rules to rapidly grow.

In this article, Toptal engineer Josip Medic shows us how validation can be decoupled from domain objects, made context-specific, and structured well to achieve more sustainable validation code.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Josip Medic
Verified Expert in Engineering

Josip is devoted to writing bug-free, maintainable, and scalable code, and is constantly educating himself about new technologies.

Expertise

PREVIOUSLY AT

PayPal
Share

Domain-driven design (DDD in short) is not a technology or a methodology. DDD provides a structure of practices and terminology for making design decisions that focus and accelerate software projects dealing with complicated domains. As described by Eric Evans and Martin Fowler, Domain objects are a place to put validation rules and business logic.

Eric Evans:

Domain Layer (or Model Layer): Responsible for representing concepts of the business, information about the business situation, and business rules. State that reflects the business situation is controlled and used here, even though the technical details of storing it are delegated to the infrastructure. This layer is the heart of business software.

Martin Fowler:

The logic that should be in a domain object is domain logic - validations, calculations, business rules - whatever you like to call it.

Context Validation in Domain-Driven Design

Putting all of validation in Domain objects results in huge and complex domain objects to work with. Personally I much prefer the idea of decoupling domain validations into separate validator components that can be reused anytime and that will be based on context and user action.

As Martin Fowler wrote in a great article: ContextualValidation.

One common thing I see people do is to develop validation routines for objects. These routines come in various ways, they may be in the object or external, they may return a boolean or throw an exception to indicate failure. One thing that I think constantly trips people up is when they think of object validity in a context independent way, such as an isValid method implies. […] I think it’s much more useful to think of validation as something that’s bound to a context , typically an action that you want to do. Such as asking if this order valid to be filled, or is this customer valid to check in to the hotel. So rather than have methods like isValid, have methods like isValidForCheckIn.

Action Validation Proposal

In this article we will implement a simple interface ItemValidator for which you need to implement a validate method with return type ValidationResult. ValidationResult is an object containing the item that has been validated and also the Messages object. The latter contains an accumulation of errors, warnings, and information validation states (messages) dependent upon the execution context.

Validators are decoupled components that can be easily reused anywhere they are needed. With this approach all dependencies, which are needed for validation checks, can be easily injected. For example, to check in the database if there is a user with given email only UserDomainService is used.

Validators decoupling will be per context (action). So if UserCreate action and UserUpdate action will have decoupled components or any other action (UserActivate, UserDelete, AdCampaignLaunch, etc.), validation can rapidly grow.

Each action validator should have a corresponding action model which will have only the allowed action fields. For creating users, the following fields are needed:

UserCreateModel:

{
  "firstName": "John",
  "lastName": "Doe",
  "email": "john.doe@gmail.com",
  "password": "MTIzNDU="
}

And to update user the following are allowed externalId, firstName and lastName. externalId is used for user identification and only changing of firstName and lastName is allowed.

UserUpdateModel:

{
  "externalId": "a55ccd60-9d82-11e5-9f52-0002a5d5c51b",
  "firstName": "John Updated",
  "lastName": "Doe Updated"
}

Field integrity validations can be shared, firstName maximum length is always 255 characters.

During validating it is desirable to not only get the first error that occurs, but a list of all issues encountered. For example, the following 3 issues may happen at the same time, and can be reported accordingly during execution:

  • invalid address format [ERROR]
  • email must be unique among users [ERROR]
  • password too short [ERROR]

To achieve that kind of validation, something like validation state builder is needed, and for that purpose Messages is introduced. Messages is a concept that I heard from my great mentor years ago when he introduced it to support validation and also for various other things that can be done with it, as Messages are not just for validation.

Note that in the following sections we will be using Scala to illustrate the implementation. Just in case you are not a Scala expert, fear not as it should be easy to follow along nonetheless.

Messages in Context Validation

Messages is an object that represents the validation state builder. It provides an easy way to collect errors, warnings, and information messages during validation. Each Messages object has an inner collection of Message objects and also can have a reference to parentMessages object.

A Message object is an object that can have type, messageText, key (which is optional and is used to support validation of specific inputs which are identified by identifier), and finally childMessages that provies a great way for building composable message trees.

A message can be of one of the following types:

  • Information
  • Warning
  • Error

Messages structured like this allows us to build them iteratively and also allows decisions to be made about next actions based on prior messages state. For example, performing validation during user creation:

@Component
class UserCreateValidator @Autowired (private val entityDomainService: UserDomainService) extends ItemValidator[UserCreateEntity] {
  Asserts.argumentIsNotNull(entityDomainService)

  private val MAX_ALLOWED_LENGTH = 80
  private val MAX_ALLOWED_CHARACTER_ERROR = s"must be less than or equal to $MAX_ALLOWED_LENGTH character"

  override def validate(item: UserCreateEntity): ValidationResult[UserCreateEntity] = {
    Asserts.argumentIsNotNull(item)

    val validationMessages = Messages.of

    validateFirstName (item, validationMessages)
    validateLastName  (item, validationMessages)
    validateEmail     (item, validationMessages)
    validateUserName  (item, validationMessages)
    validatePassword  (item, validationMessages)

    ValidationResult(
      validatedItem = item,
      messages      = validationMessages
    )
  }

  private def validateFirstName(item: UserCreateEntity, validationMessages: Messages) {
    val localMessages = Messages.of(validationMessages)

    val fieldValue = item.firstName

    ValidateUtils.validateLengthIsLessThanOrEqual(
      fieldValue,
      MAX_ALLOWED_LENGTH,
      localMessages,
      UserCreateEntity.FIRST_NAME_FORM_ID.value,
      MAX_ALLOWED_CHARACTER_ERROR
    )
  }

  private def validateLastName(item: UserCreateEntity, validationMessages: Messages) {
    val localMessages = Messages.of(validationMessages)

    val fieldValue = item.lastName

    ValidateUtils.validateLengthIsLessThanOrEqual(
      fieldValue,
      MAX_ALLOWED_LENGTH,
      localMessages,
      UserCreateEntity.LAST_NAME_FORM_ID.value,
      MAX_ALLOWED_CHARACTER_ERROR
    )
  }

  private def validateEmail(item: UserCreateEntity, validationMessages: Messages) {
    val localMessages = Messages.of(validationMessages)

    val fieldValue = item.email

    ValidateUtils.validateEmail(
      fieldValue,
      UserCreateEntity.EMAIL_FORM_ID,
      localMessages
    )

    ValidateUtils.validateLengthIsLessThanOrEqual(
      fieldValue,
      MAX_ALLOWED_LENGTH,
      localMessages,
      UserCreateEntity.EMAIL_FORM_ID.value,
      MAX_ALLOWED_CHARACTER_ERROR
    )

    if(!localMessages.hasErrors()) {
      val doesExistWithEmail = this.entityDomainService.doesExistByByEmail(fieldValue)
      ValidateUtils.isFalse(
        doesExistWithEmail,
        localMessages,
        UserCreateEntity.EMAIL_FORM_ID.value,
        "User already exists with this email"
      )
    }
  }

  private def validateUserName(item: UserCreateEntity, validationMessages: Messages) {
    val localMessages = Messages.of(validationMessages)

    val fieldValue = item.username

    ValidateUtils.validateLengthIsLessThanOrEqual(
      fieldValue,
      MAX_ALLOWED_LENGTH,
      localMessages,
      UserCreateEntity.USERNAME_FORM_ID.value,
      MAX_ALLOWED_CHARACTER_ERROR
    )

    if(!localMessages.hasErrors()) {
      val doesExistWithUsername = this.entityDomainService.doesExistByUsername(fieldValue)
      ValidateUtils.isFalse(
        doesExistWithUsername,
        localMessages,
        UserCreateEntity.USERNAME_FORM_ID.value,
        "User already exists with this username"
      )
    }
  }

  private def validatePassword(item: UserCreateEntity, validationMessages: Messages) {
    val localMessages = Messages.of(validationMessages)

    val fieldValue = item.password

    ValidateUtils.validateLengthIsLessThanOrEqual(
      fieldValue,
      MAX_ALLOWED_LENGTH,
      localMessages,
      UserCreateEntity.PASSWORD_FORM_ID.value,
      MAX_ALLOWED_CHARACTER_ERROR
    )
  }
}

Looking into this code, you can see use of ValidateUtils. These utility functions are used to populate the Messages object in predefined cases. You can check out the implementation of ValidateUtils on Github code.

During email validation, first it is checked if email is valid by calling ValidateUtils.validateEmail(…, and it is also checked if email has a valid length by calling ValidateUtils.validateLengthIsLessThanOrEqual(… . Once these two validations are done, checking if the email is already assigned to some User is performed, only if the prior email validation conditions are passing and that is done with if(!localMessages.hasErrors()) { … . This way expensive database calls can be avoided. This is only part of UserCreateValidator. Complete source code can be found here.

Notice that one of the validation parameters stand out: UserCreateEntity.EMAIL_FORM_ID. This parameter connects validation state to a specific input ID.

In previous examples, next action is decided based upon the fact if Messages object has errors (using hasErrors method). One can easily check if there are any “WARNING” messages and retry if necessary.

One thing that can be noticed is the way localMessages is used. Local messages are messages that have been created the same as any message, but with parentMessages. With that said, the goal is to have a reference only to current validation state (in this example emailValidation), so localMessages.hasErrors can be called, where it is checked only if emailValidation context hasErrors. Also when a message is added to localMessages, it is also added to parentMessages and so all validation messages exist in higher context of UserCreateValidation.

Now that we have seen Messages in action, in next chapter we will focus on ItemValidator.

ItemValidator - Reusable Validation Component

ItemValidator is a simple trait (interface) that forces developers to implement the method validate, which needs to return ValidationResult.

ItemValidator:

trait ItemValidator[T] {
  def validate(item:T) : ValidationResult[T]
}

ValidationResult:

case class ValidationResult[T: Writes](
  validatedItem : T,
  messages      : Messages
) {
  Asserts.argumentIsNotNull(validatedItem)
  Asserts.argumentIsNotNull(messages)

  def isValid :Boolean = {
    !messages.hasErrors
  }

  def errorsRestResponse = {
    Asserts.argumentIsTrue(!this.isValid)

    ResponseTools.of(
      data      = Some(this.validatedItem),
      messages  = Some(messages)
    )
  }
}

When ItemValidators such as UserCreateValidator are implemented to be dependency injection components, then ItemValidator objects can be injected and reused in any object that needs UserCreate action validation.

After validation is executed, it is checked if validation was successful. If it is, then user data is persisted to database, but if not the API response containing validation errors is returned.

In the next section we will see how we can present validation errors in RESTful API response and also how to communicate with API consumers about execution action states.

Unified API Response - Simple User Interaction

After user action has been successfully validated, in our case user creation, validation action results need to be displayed to RESTful API consumer. The best way is to have a unified API response where only context will be switched (in terms of JSON, value of “data”). With unified responses, errors can be presented easily to RESTful API consumers.

Unified response structure:

{
    "messages" : {
        "global" : {
            "info": [],
            "warnings": [],
            "errors": []
        },
        "local" : []
    },
	"data":{}
}

Unified response is structured to have two tiers of messages, global and local. Local messages are messages that are coupled to specific inputs. Such as “username is too long, at most 80 characters is allowed”_. Global messages are messages that are reflecting the state of whole data on page, such as “user will not be active until approved”. Local and global messages have three levels - error, warning and information. The value of “data” is specific to the context. When creating users the data field will contain user data, but when getting a list of users the data field will be an array of users.

With this kind of structured response the client UI handler can be created easily, which will be responsible for displaying errors, warnings, and information messages. Global messages will be displayed at the top of the page, because they are related to global API action state, and local messages can be displayed near specified input (field), as they are directly related to the field’s value. Error messages can be presented in red color, warning messages in yellow, and information in blue.

For example, in an AngularJS based client app we can have two directives responsible for handling local and global response messages, so that only these two handlers can deal with all responses in a consistent manner.

Directive for the local message will need to be applied to an element parent to the actual element holding all the messages.

localmessages.direcitive.js :

(function() {
  'use strict';

  angular
    .module('reactiveClient')
    .directive('localMessagesValidationDirective', localMessagesValidationDirective);

  /** @ngInject */
  function localMessagesValidationDirective(_) {
    return {
      restrict: 'AE',
      transclude: true,
      scope: {
        binder: '='
      },
      template: '<div ng-transclude></div>',
      link: function (scope, element) {

        var messagesWatchCleanUp = scope.$watch('binder', messagesBinderWatchCallback);
        scope.$on('$destroy', function() {
          messagesWatchCleanUp();
        });

        function messagesBinderWatchCallback (messagesResponse) {
          if (messagesResponse != undefined && messagesResponse.messages != undefined) {
            if (messagesResponse.messages.local.length > 0) {
              element.find('.alert').remove();
              _.forEach(messagesResponse.messages.local, function (localMsg) {

                var selector = element.find('[id="' + localMsg.inputId + '"]').parent();

                _.forEach(localMsg.info, function (msg) {
                  var infoMsg = '<div class="form-control validation-alert alert alert-info alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>' + msg + '</div>';
                  selector.after(infoMsg);
                });

                _.forEach(localMsg.warnings, function (msg) {
                  var warningMsg = '<div class="form-control validation-alert alert alert-warning alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>' + msg + '</div>';
                  selector.after(warningMsg);
                });

                _.forEach(localMsg.errors, function (msg) {
                  var errorMsg = '<div class="form-control validation-alert  alert alert-danger alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>' + msg + '</div>';
                  selector.after(errorMsg);
                });
              });
            }
          }
        }
      }
    }
  }

})();

Directive for global messages will be included in the root layout document (index.html) and will register to an event for handling all global messages.

globalmessages.directive.js:

(function() {
  'use strict';

  angular
    .module('reactiveClient')
    .directive('globalMessagesValidationDirective', globalMessagesValidationDirective);

  /** @ngInject */
  function globalMessagesValidationDirective(_, toastr, $rootScope, $log) {
    return {
      restrict: 'AE',
      link: function (scope) {

        var cleanUpListener = $rootScope.$on('globalMessages', globalMessagesWatchCallback);
        scope.$on('$destroy', function() {
          cleanUpListener();
        });

        function globalMessagesWatchCallback (event, messagesResponse) {
          $log.log('received rootScope event: ' + event);
          if (messagesResponse != undefined && messagesResponse.messages != undefined) {
            if (messagesResponse.messages.global != undefined) {
              _.forEach(messagesResponse.messages.global.info, function (msg) {
                toastr.info(msg);
              });

              _.forEach(messagesResponse.messages.global.warnings, function (msg) {
                toastr.warning(msg);
              });

              _.forEach(messagesResponse.messages.global.errors, function (msg) {
                toastr.error(msg);
              });
            }
          }
        }
      }
    }
  }

})();

For a more complete example, let us consider the following response containing a local message:

{
    "messages" : {
        "global" : {
            "info": [],
            "warnings": [],
            "errors": []
        },
        "local" : [
           {
                "inputId" : "email",
                "errors" : ["User already exists with this email"],
                "warnings" : [],
                "info" : []
            }
        ]
    },
	"data":{
	    "firstName": "John",
	    "lastName": "Doe",
	    "email": "john.doe@gmail.com",
      "password": "MTIzNDU="
    }
}

The above response can lead to something as follows:

On the other hand, with a global message in response:

{
    "messages" : {
        "global" : {
            "info": ["User successfully created."],
            "warnings": ["User will not be available for login until is activated"],
            "errors": []
        },
        "local" : []
    },
	"data":{
	    "externalId": "a55ccd60-9d82-11e5-9f52-0002a5d5c51b",
	    "firstName": "John",
	    "lastName": "Doe",
	    "email": "john.doe@gmail.com"
    }
}

The client app can now show the message with more prominence:

In above examples it can be seen how a unified response structure can be handled for any request with the same handler.

Conclusion

Applying validation on large projects can become confusing, and validation rules can be found everywhere throughout project code. Keeping validation consistent and well structured makes things easier and reusable.

You can find these ideas implemented in two different versions of boilerplates below:

In this article I have presented my suggestions on how to support deep, composable context validation which can be easily presented to a user. I hope this will help you solve the challenges of proper validation and error handling once and for all. Please feel free to leave your comments and share your thoughts below.

Hire a Toptal expert on this topic.
Hire Now
Josip Medic

Josip Medic

Verified Expert in Engineering

Split, Croatia

Member since June 18, 2014

About the author

Josip is devoted to writing bug-free, maintainable, and scalable code, and is constantly educating himself about new technologies.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

PREVIOUSLY AT

PayPal

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.