A simple pattern to keep your business logic together

#craftsmanship Mar 30, 2023 5 min Mike Kowalski

Keeping our domain (business) logic together is usually a really good idea. It not only makes it easier to reason about the most important part of our code but also increases its cohesion. Yet, decoupling the domain code from all the rest (orchestration, persistence, etc.) can be tricky.

In this post, I’d like to share a simple pattern that helps me with that. The amazing Unit Testing Principles, Practices, and Patterns book calls it the CanExecute/Execute pattern.

Example

Imagine a social media platform that would like to follow its competitors and limit some of its functionality to the “premium” customers. In this case, only the paying users should be able to change their usernames. Our first attempt is quite straightforward:

class User {
    private UUID id;
    private String username;
    private UserType type;
        
    // ...

    UserType getType() {
        return type;
    }

    void setUsername(String username) {
        this.username = username;
    }
}


class UserService {

    // ...

    void changeUsername(UUID userId, String updatedUsername) {
        User user = userRepository.getById();
        if (user.type != UserType.PAYING) {
            // probably combined with an exception handler 
            // to present a meaningful error message
            throw new PaidFeatureUnavailableException();
        }
    
        user.setUsername(updatedUsername);
        userRepository.save(user);
    }
   
}

The change has been rolled out and no one got publicly fired 😉 So far so good. Unfortunately, there are a couple of things I don’t like about this code.

The type check present in our service class doesn’t prevent changing the username from somewhere else. Thus, it’s still possible to change (set) the username of a non-paying customer, breaking our domain rules. Performing actions illegal from the domain point of view should not be possible even from the test code.

Secondly, the user-related logic has leaked to the corresponding service class. Let’s try to fix that:

class User {
        
    // ...
    
    // no more setUsername(...) method!

    void changeUsername(String updatedUsername) {
        if (type != UserType.PAYING) {
            throw new PaidFeatureUnavailableException();
        }
            
        this.username = updatedUsername;
    }
}

class UserService {

    // ...

    void changeUsername(UUID userId, String updatedUsername) {
        User user = userRepository.getById();
        // what about the error handling???
        user.changeUsername(updatedUsername);
        userRepository.save(user);
    }
    
}

All our domain logic is now contained within the User class. By removing the username setter, it’s now impossible to change its value for free users (just forget about the reflection). This prevents violating domain constraints even from the test code. Last but not least, our service got simpler.

At the same time, the UserService lost control of the error handling. In case of a failed precondition, the domain User class will just throw an exception, interrupting the execution flow. Our service may ignore it or catch it and do something else. None of them feels like a “clean” approach.

If there’s a need for a custom response (message or HTTP error code) when the condition fails, one may think of adding an exception handler on top. Yet, this makes the presentation layer dependent on something that happens deeper in the domain layer. This might be considered as mixing different levels of abstraction.

Of course, one could say that using exceptions here is not the brightest idea. As an alternative, we could change our User#changeUsername method’s return type to something indicating success or failure. There are many solutions available (including Either), but the simplest one would probably be a good-old boolean:

class User {
        
    // ...
    
    boolean changeUsername(String updatedUsername) {
        if (type != UserType.PAYING) {
            return false;
        }
            
        this.username = updatedUsername;
        return true;
    }
}

class UserService {

    // ...

    void changeUsername(UUID userId, String updatedUsername) {
        User user = userRepository.getById();
        if (!user.changeUsername(updatedUsername)) {
            // error handling logic here
        }
        userRepository.save(user);
    }
    
}

Yet, I still don’t like this approach. Calling changeUsername may or may not succeed. It feels like tryToChangeUsername would be a more appropriate name… But it’s not about the naming here. We just introduced extra complexity to this method together with an implicit decision-making process.

Even now, nothing prevents the caller from ignoring the method’s response. In such a case, the missed username update would simply go unnoticed. This seems to be a potential source of problems that would be quite hard to spot.

CanExecute/Execute pattern

The idea behind the CanExecute/Execute pattern is extremely simple. Let’s split the decision-making (precondition check) from performing the actual operation, but keep them both in the domain object. Also, let’s prevent performing the operation if the precondition is not met.

This is how it looks like for our example:

class User {
        
    // ...
    
    // can be called from the outside without side effects
    boolean canChangeUsername() {
        return type == UserType.PAYING;
    }

    void changeUsername(String updatedUsername) {
        // fails if not met! (domain assertion)
        require(canChangeUsername(), "Illegal username change"); 
            
        this.username = updatedUsername;
    }
}

class UserService {

    // ...

    void changeUsername(UUID userId, String updatedUsername) {
        User user = userRepository.getById();
        if (!user.canChangeUsername()) {
            // error handling logic here
        }
        user.changeUsername(updatedUsername);
        userRepository.save(user);
    }
    
}

The concept of the require function should be familiar to those who worked with languages like Kotlin before. Luckily, it’s not hard to implement it in Java:

static void require(boolean precondition, String errorMessage) {
    if (!precondition) {
        throw DomainPreconditionFailed(errorMessage);
    }
}

Changing the username is still impossible for non-paying users. Yet, we can now easily check (with User#canChangeUsername) if the action is allowed or not without any consequences or side effects. It’s up to the caller what to do in each of the cases.

Throwing an exception when the precondition is not met should not be considered a bad smell. Compared to our previous examples, this exception has a whole different meaning. The only case when the DomainPreconditionFailed could be thrown is a faulty implementation. It indicates a situation that should never happen, as all the callers are expected to always call canChangeUsername first. In other words, the require call represents our domain assertion.

Note: Kotlin’s require function throws IllegalArgumentException when the condition was not met. Introducing a dedicated DomainPreconditionFailed exception allows us to distinguish it from exceptions coming from other parts of the code.

Of course, we’re now running the precondition check twice. Luckily, logic like this is rarely computation-intensive. It shouldn’t involve any I/O operations either, as all the inputs can be passed as parameters. Therefore, we are paying a relatively small price for an additional level of domain safety. Yet, those obsessed with performance may need to look for something else (probably including a completely different style of organizing their code).

Summary

The CanExecute/Execute pattern introduces the concept of a domain assertion to our code. By exposing the precondition check to a separate function, the callers may always validate the input before performing an action and react accordingly. At the same time, performing an action is no longer possible without checking the precondition first.

The additional benefit of this approach is moving the decision-making process from service to the domain code. This not only keeps our domain logic together, but also promotes its unit testing. At the same time, the simplified service may no longer need so much attention. A single integration test “touching” that code could be more than enough. However, that’s a topic for a separate post.

Mike Kowalski

Software engineer believing in craftsmanship and the power of fresh espresso. Writing in & about Java, distributed systems, and beyond. Mikes his own opinions and bytes.