Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / performance

Business Validation Techniques on SOA

4.00/5 (1 vote)
14 Apr 2013CPOL6 min read 15.1K  
A point of view about business validations on enterprise applications

Introduction 

Most of the computer systems are designed to be accessed initially by a few clients like UI, and some batch processes, when the application interface number grows (for example allowing RMI, WebServices, ResFull, etc), the validations and even the business rules get complex, and sometimes developers need refactor some pieces of software to maintain consistency.

Some systems should adapt to handle different business rules on different environments, and those rules may impact on the validation of the incoming commands, and validation mechanisms gains comeplexity.   

While some frameworks guide us to implement data validations in some specific way, most of the time the decision is taken by architects. These examples will show some basic validation principles to be considered.

Background  

The first advice is: implement each external access to the system across MVC.
The MVC pattern takes some responsibilities related to validations in every section: 

  • The View is responsible of data collection, should collect the enough amount of information to execute a comand on a controller, and there is no obligated responsibility to validating any of the input values, think about batch process that can access to our controllers and does not have any input validation.
    To improve user interaction on views that perform interactions with human users, it's desired to have some input type data cheek to prevent empty values, to avoid invalid data types or to check correct data format, etc. This kind of human user validations are desired to prevent incorrect command calls.
  • The Controller has the responsibility to get values of the request, check the correct data type and perform data conversions if are needed. Also the controller could check some architecture limitations on the input data, validations like max value, min value, length, invalid characters, or null values. With this information che controller fills the Command Value Object and execute the command in the model.
  • The Model receives the requested command with the correct data type as expected, so its responsibility is to check every business rule and architecture limitations of each data type before apply any system status change. 

Do you know the concept of Fat Model, Skinny Controller? Basically says that the controller should be as simple as possible, and limits its behavior to something like :

  • Parse requested data types  
  • Call the model
  • Process the model response
  • Build the context for the view
  • Call the view

I will use a simple concept to focus on validations.

To simplify implementation and to not deal with in any framework specific issue, i will implement the controllers using servlets, and everyone can adapt this solution to most used frameworks and model access techniques.

Simple Solution  

Imagine that we have a selling portal that allows our clients buy items, and we will work on the process to buy an item, the entities are represented by: Client, Item and Buy.

The Controller is represented by BuyController.

A simple way, in which a controller interacts with the model, could be:

Controller : 

Java
 /**
 * This routine will handle the post mechanism of a UI view. 
 * The post will process the buy item commit.
 */
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // TODO: Perform security/access validations
    
    
    // FIRST VALIDATION BLOCK.
    Map<String, String> dataTypeValidationErrors = new HashMap<String, String>();
    
    // Validates the correct data type of itemId, also check that an item is selected.
    Integer itemCode = NumberUtils.toInt(request.getParameter("itemId"), 0);
    if(itemCode == 0) {
        dataTypeValidationErrors.put("itemIdError", "Select an item to buy.");
    }

    // Validates how much items will buy.
    Integer itemQuantity = NumberUtils.toInt(request.getParameter("itemQuantity"), 0);
    if(itemQuantity == 0) {
        dataTypeValidationErrors.put("itemQuantityError", "Select how many items will buy.");
    }
    
    // And validate the payment method.
    Integer paymentMethodCode = NumberUtils.toInt(request.getParameter("paymentMethodId"), 0);
    if(paymentMethodCode == 0) {
        dataTypeValidationErrors.put("paymentMethodIdError", "Select the payment method.");
    }
    
    // On basic errors, early reject to the view, with the error messages
    if(dataTypeValidationErrors.size() > 0) {
        for(String key: dataTypeValidationErrors.keySet()) {
            request.setAttribute(key, dataTypeValidationErrors.get(key));
        }
        // TODO: Redirect to same view, with the error messages.
    }

    
    // SECOND VALIDATION BLOCK
    // Perform business validations on items.
    dataTypeValidationErrors = buyModelService.validateBuyItem(itemCode, itemQuantity);
    if(dataTypeValidationErrors.size() > 0) {
        for(String key: dataTypeValidationErrors.keySet()) {
            request.setAttribute("itemQuantityError", dataTypeValidationErrors.get(key));
        }
    }
    
    // Perform business validations on payment method.
    dataTypeValidationErrors = buyModelService.validatePaymentMethod(paymentMethodCode);
    if(dataTypeValidationErrors.size() > 0) {
        for(String key: dataTypeValidationErrors.keySet()) {
            request.setAttribute("paymentMethodIdError", dataTypeValidationErrors.get(key));
        }
    }
    
    // On business errors, reject the process to the view, showing error messages.
    if(dataTypeValidationErrors.size() > 0) {
        for(String key: dataTypeValidationErrors.keySet()) {
            request.setAttribute(key, dataTypeValidationErrors.get(key));
        }
        // TODO: redirect to view.
    }

    // If everything in ok, call buyItem
    BuyItemCommand biCommand = new BuyItemCommand();
    biCommand.setItemId(itemCode);
    biCommand.setItemQuantity(itemQuantity);
    biCommand.setPaymentMethodId(paymentMethodCode);
    try {
        Buy performedBuy = buyModelService.buyItem(biCommand);
        request.setAttribute("buy", performedBuy);
        
        // TODO: redirect to the correct view informing that the buy was done correctly.
        
    } catch (BuyException | ParameterValidationException ex) {
        request.setAttribute("error", ex.getMessage());

        // TODO: redirect to the view.
    } catch (Exception ex) {
        // TODO: log the error and redirect to general failure view.
    }
} 

When the cotroller set request attributes like 'itemIdError', 'itemQuantityError' and 'paymentMethodIdError' i bound values to fields that shows the error message in the correct UI form location. 'error' is a generic error to be sown in the UI.


Business Service : 

Java
public Buy buyItem(BuyItemCommand biCommand) throws ParameterValidationException, BuyException {
    // Business validations will be sorted by complexity, throwing exceptions early.

    // Get Client from session info.
    Customer currentCustomer = sessionInfo.getCurrentCustomer();
    if(currentCustomer == null) {
        throw new SecurityException("Client not logged In.");
    }	
    
    // Validate if Item Exists.
    Item item = itemRepository.get(biCommand.getItemId());
    if(item == null) {
        throw new ParameterValidationException("itemId", "Select a valid Item to Buy.");
    }
    
    // Validate payment Method
    PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
    if(paymentMethod == null) {
        throw new ParameterValidationException("paymentMethodId", "Select a valid payment method.");
    }
	
    // Perform business validations on items.
    if(validateBuyItem(item, biCommand.getItemQuantity()).size() > 0) {
        throw new ParameterValidationException("itemId", "Select a valid Item to Buy.");
    }
    if(validatePaymentMethod(paymentMethod).size() > 0) {
        throw new ParameterValidationException("paymentMethodId", "Select a valid payment method.");
    }

    
    // MORE VALIDATIONS : Example : Validate if the item, customer, quantity and payment method is possible.
    if(validatePaymentMethodForItem(paymentMethod, item).size() > 0) {
        throw new BuyException("You cannot buy this item with the specified payment method.");
    }
    
    // This simplified logic will store the buy in repository.
    BuyBuilder buyBuilder = new BuyBuilder();
    buyBuilder.setCustomer(currentCustomer);
    buyBuilder.setItem(item);
    buyBuilder.setPaymentMetod(paymentMethod);
    buyBuilder.setQuantity(biCommand.getItemQuantity());
    
    return buyRepository.add(buyBuilder.build());
}  

On the previous example the validations and rules are checked in three blocks:
  • The validation on the first block  is about incoming data types.
  • The validation on second block is performed on the business layer, and related to business rules.
  • Finally the buyItem method throws an exception if something fails in an unexpected way.

The first block is inevitable, every controller should validate data input and adapt them to call the correct command. It's a good idea to implement some library to unify the conversions in the architecture layer.

The second block validates business rules, but also defines a validation flow, and this is undesired to do in the controllers, suppose that validation changes depending on context, the validations procedure should have a complex logic and it's unnecessary implemented on each controller.

Also, sometimes the UI team is not the same as the model team, moving validation flow to the controllers team means an interaction between model developers and UI developers that can be avoided.

Basically the problem is that the validation flow that the model needs to do on buyItem is transferred to the controllers.

Validation Handler

A better design would be to implement a business validation handler to validate commands, this will follow the validation flow for the buyItem event considering the context that it's executed. Doesn't means that field validations should desapear, when the environment needs validation of each data input performed by the client in real time, then the validation handler could expose methods to validate each UI field data, but when the user sends the command to commit the operation, the server should run the correct validation on demand.

Controller:

Java
/**
 * The post will commit the buy.
 */
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // TODO: Perform security/access validations
    
    // Validates the parameters.
    Map<String, String> basicValidationErr = validateInputDataTypes(request);
    if(basicValidationErr.size() > 0) {
        for(String key: basicValidationErr.keySet()) {
            request.setAttribute(key, basicValidationErr.get(key));
        }
        // TODO: return to view with error messages.
    }

    // Build the Buy Request.
    BuyItemCommand biCommand = new BuyItemCommand();
    biCommand.setItemId(NumberUtils.toInt(request.getParameter("itemCode")));
    biCommand.setItemQuantity(NumberUtils.toInt(request.getParameter("itemQuantity")));
    biCommand.setPaymentMethodId(NumberUtils.toInt(request.getParameter("paymentMethodCode")));
    
    // Validates the Request calling the business validator.
    Map<String, String> bssValidationErr = buyItemValidator.validateBuyItem(biCommand);
    if(bssValidationErr.size() > 0) {
        for(String key: bssValidationErr.keySet()) {
            if(key.equals("itemCode")) {
                request.setAttribute("itemCodeError", bssValidationErr.get(key));
            } else if (key.equals("itemQuantity")) {
                request.setAttribute("itemQuantityError", bssValidationErr.get(key));
            } else if (key.equals("paymentMethodCode")) {
                request.setAttribute("paymentMethodCodeError", bssValidationErr.get(key));
            } 
        }
        
        //TODO: return to the view with the error messages
    }
    
    // Call the business proceess
    try {
        Buy performedBuy = buyModelService.buyItem(biCommand);
        request.setAttribute("buy", performedBuy);
        
        // TODO: redirect to the correct view informing that the buy was done correctly.
        
    } catch (BuyException | ParameterValidationException ex) {
        request.setAttribute("error", ex.getMessage());

        // TODO: return to the view showing the error message.
    } catch (Exception ex) {
        // TODO: log the error and redirect to general failure view.
    }
}  

Validator (BuyItemValidator.java) :
Java
/**
 * Validates the buyItem operation
 * 
 * @param biCommand The buy command
 * @return Map of biCommand properties bounded errors.
 */
public Map<String, String> validateBuyItem(BuyItemCommand biCommand) {
     Map<String, String> problems = new HashMap<String, String>();
     
    // Validate if Item Exists.
    Item item = itemRepository.get(biCommand.getItemId());
    if(item == null) {
        problems.put("itemId", "Select a valid Item to Buy.");
    }
    
    // Validate payment Method
    PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
    if(paymentMethod == null) {
        problems.put("paymentMethodId", "Select a valid payment method.");
    }

    // If some problem is detected here, return early.
    if(!problems.isEmpty()) {
        return problems;
    }
        	 
    // Perform business validations on items.
    if(validateItemQuantity(item, biCommand.getItemQuantity()).size() > 0) {
        problems.put("itemId", "Select a valid Item to Buy.");
    }
    if(validatePaymentMethod(paymentMethod).size() > 0) {
        problems.put("paymentMethodId", "Select a valid payment method.");
    }

    // Example : Validate if the item, customer, quantity and payment method is possible.
    if(validatePaymentMethodForItem(item, paymentMethod).size() > 0) {
        problems.put("paymentMethodId", "You cannot buy this item with the specified payment method.");
    }
    
    return problems;
} 

And Business Service :
Java
public Buy buyItem(BuyItemRequest bIRequest) {
    // Get Client from session info.
    Customer currentCustomer = sessionInfo.getCurrentCustomer();
    if(currentCustomer == null) {
        throw new SecurityException("Client not logged In.");
    }

    // Check the same validations.
    Map<String, String> problems = buyItemValidator.validateBuyItem(bIRequest);
    if(!problems.isEmpty()) {
        throw new BuyException("Your buy cannot be performed.");
    }
    
    // Build the Buy and store it.
    Item item = itemRepository.get(bIRequest.getItemId());
    PaymentMethod paymentMethod = paymentMethodRepository.get(bIRequest.getPaymentMethodId());
    
    BuyBuilder buyBuilder = new BuyBuilder();
    buyBuilder.setCustomer(currentCustomer);
    buyBuilder.setItem(item);
    buyBuilder.setPaymentMetod(paymentMethod);
    buyBuilder.setQuantity(bIRequest.getItemQuantity());
    
    return buyRepository.add(buyBuilder.build());
}


In this example I split the data type validations in a new method, also define a business method that receives the same object that the final business method, so any validation can be done in this validator, and most important, we solve the validation flow in the model layer.

This new class BuyItemValidator will validate the command buyItem, receiving the same parameters that the buyItem will receive with the same context. This handler centralizes the validation flow, and also enables different business rules on different contexts, fitting different client needs.

BuyItem method is optimistic, calling validations on handlers, it permits a clean focus on one objetive: save the buy.

Can we ask more? Yes sure... 

The Transactional Problem

With this approach our enterprise application will feel powerful, but may fall in unexpected results on concurrent access, the validation could success the initial validation, but fail when the business method is invoked. Think about the item availability, when you call the validator maybe the provider has the enough stock to perform the sell, but when the event is invoked it fails, someone else commit a buy between the validation and the method call. So, would be better if the validations run inside the buyItem method then we could block stock in the same transaction, in the meanwile could return the error or success.

Also another problem is solved with this validation procedure, the validation code runs only once performing a bettwe use of the server resources; with the previous implementations some validations runs twice.

One way to handle this situation is to encapsulate the response into a Response object that could have the response itself and a list of errors, something like this:

Model :

Java
public BuyResponse buyItem(BuyItemCommand biCommand) throws BuyException {
    BuyResponse response = new BuyResponse();

    // Get Client from session info.
    Customer currentCustomer = sessionInfo.getCurrentCustomer();
    if(currentCustomer == null) {
        throw new SecurityException("Client not logged In.");
    }
    
    // Check the same validations.
    Map<String, String> problems = buyItemValidator.validateBuyItem(biCommand);
    if(!problems.isEmpty()) {
        response.setValidationProblems(problems);
        return response;
    }
    
    // Build the Buy and store it.
    Item item = itemRepository.get(biCommand.getItemId());
    PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
    
    BuyBuilder buyBuilder = new BuyBuilder();
    buyBuilder.setCustomer(currentCustomer);
    buyBuilder.setItem(item);
    buyBuilder.setPaymentMetod(paymentMethod);
    buyBuilder.setQuantity(biCommand.getItemQuantity());
    
    response.setBuy(buyRepository.add(buyBuilder.build()));
    return response;
}  

The business method should be able to respond the object if everything is ok, and the list of validation errors. This is the functionality of BuyResponse, encapsulate the object for success calls or error list if something is wrong. The controller now gets the errors inside the BuyResponse, or the correct Buy when success.

There is an alternative, sometimes the power of Exceptions is underestimated, the Exception mechanism is a powerful tool that in good hands do magic, suppose that we have an special ParameterValidationException  that contains all the details about the validation errors, lets implement the Model like this :


Model:

Java
public Buy buyItem(BuyItemCommand biCommand) {
	BuyResponse response = new BuyResponse();

	// Get Client from session info.
	Customer currentCustomer = sessionInfo.getCurrentCustomer();
	if(currentCustomer == null) {
		throw new SecurityException("Client not logged In.");
	}
	
	// Check the same validations.
	Map<String, String> problems = buyItemValidator.validateBuyItem(biCommand);
	if(!problems.isEmpty()) {
		throw new ParameterValidationException(problems);
	}
	
	// Build the Buy and store it.
	Item item = itemRepository.get(biCommand.getItemId());
	PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
	
	BuyBuilder buyBuilder = new BuyBuilder();
	buyBuilder.setCustomer(currentCustomer);
	buyBuilder.setItem(item);
	buyBuilder.setPaymentMetod(paymentMethod);
	buyBuilder.setQuantity(biCommand.getItemQuantity());
	
    return buyRepository.add(buyBuilder.build());
}
  


Controller :

Java
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // TODO: Perform security/access validations

    // If some basic data type is not filled, will return to the same view, with the errors
    Map<String, String> basicValidationErr = validateInputDataTypes(request);
    if(basicValidationErr.size() > 0) {
        for(String key: basicValidationErr.keySet()) {
            request.setAttribute(key, basicValidationErr.get(key));
        }
        // TODO: return to view with error messages.
    }

    // Build the Buy Request.
    BuyItemCommand biCommand = new BuyItemCommand();
    biCommand.setItemId(NumberUtils.toInt(request.getParameter("itemCode")));
    biCommand.setItemQuantity(NumberUtils.toInt(request.getParameter("itemQuantity")));
    biCommand.setPaymentMethodId(NumberUtils.toInt(request.getParameter("paymentMethodCode")));
    
    // Call the business proceess
    try {
        Buy currentBuy = buyModel.buyItem(biCommand);
        request.setAttribute("buy", currentBuy);
        
        //TODO: forward to the correct view informing that the buy was done correctly.

    } catch (ParameterValidationException pe) {            
        // If some error parameter validation error happends, will receive this special Exception.
        for(String key: pe.getValidationErrors().keySet()) {
            if(key.equals("itemCode")) {
                request.setAttribute("itemCodeError", pe.getValidationErrors().get(key));
            } else if (key.equals("itemQuantity")) {
                request.setAttribute("itemQuantityError", pe.getValidationErrors().get(key));
            } else if (key.equals("paymentMethodCode")) {
                request.setAttribute("paymentMethodCodeError", pe.getValidationErrors().get(key));
            } 
        }

        //TODO: forward to the correct view informing the validation errors.
        
    } catch (BuyException ex) {
        request.setAttribute("error", ex.getMessage());
        //TODO: forward to the correct view informing the validation errors. 
       
    } catch (Exception ex) {
        // TODO: log the error and redirect to general failure view
    }
} 

These implementation 'breaks' some exception rules defining an special exception for parameter validation errors, that Exception will be thrown on validation problems, and internally has complete information about problems, like a map with each request object property and the error message. 

Finally a solution based on Exceptions allows business validations implemented with aspects easy, running business validations before method execution.

Enjoy Validations.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)