Mutations: A Python Package for Business Logic

Feb 17, 2018

Mutations is a Python package I just released for handling, validating, and executing complex business logic in web applications. I’ve used it in a large Django codebase to shrink large models and move complex logic into independent, testable, and reusable command classes. It’s based on the Ruby eponym, Jonathan Novak‘s excellent mutations gem.

Check it out on GitHub and PyPi.

Why Use Mutations?

  1. Keep Models & Views Thin: Mutations is a library for your business logic. It encourages you to keep business logic in a dedicated location instead of deeply boring it in model or view code.
  2. Makes Key Transactions Testable: Commands in Mutations simple to test; they’re regular Python classes. You can easily test them using unittest, pytest, or your favorite test runner. Easy test setup means your team can focus on edge cases that impact your customers; not getting your test suite to simulate an rare edge condition.
  3. Validates User Inputs: Mutations comes with a set of input validators, so you get input validation for free on all of your commands. For example, you can define character, email, dict, and essentially any other sort of field. Expecting an email address? There’s a validator for that.
  4. Modular: If you’re doing similar transactions over and over again, you can factor out shared logic and keep it one place so that you don’t repeat yourself.
  5. Extensible: You can write custom validations to test against any condition. Moreover, the core library is ~200 lines of code, so it’s easy to fork and make changes to fit any bespoke needs.

What is Business Logic?

Business logic: rules and conditions that run your app/business and make the user’s experience predictable and streamlined.

As an example (in a web application): when a user signs up, send them a welcome email, except if they signed up with their Twitter account, in which case send them a twitter DM. Lots of these small decisions and policies add up and sometimes lead to very large models.

This logic is simple in the beginning, but can quickly grow as your business needs evolve, your previously simple logic gets complex, very quickly. For example, if the new user comes through a partner’s channel, make an API request to that partner’s backend and see if they should get an email or not. Sometimes, the partner’s backend is down, in which case enqueue the request and try again in five minutes.

Complexity increases quickly, and suddenly you find yourself enqueueing retry requests to your partner’s backend in a class that also handles storing the user’s email and password. With this we face a new question: where should this code live?

Store Logic in Separate Command Classes

I wrote Mutations as an answer to this question: store the business logic in separate, reusable, and testable classes. Classes with fewer responsibilities are generally easier to maintain, test, and debug, and pulling transaction logic out of models and into something else keeps your models focused on what they do best: defining and persisting data.

Here’s an example of how you could use it to handle the aforementioned example:

import mutations

class UserSignup(mutations.Mutation):
    name = mutations.fields.CharField()
    email = mutations.fields.CharField()

    def execute(self):
        user = User.objects.create([...])
        if user.is_referred_through_partner:
            [...]
        else:         
            self.send_welcome_notification()

    def send_welcome_notification(self):
        if user.is_twitter_signup:
            self.send_message(through='twitter')
        else:
            self.send_message(through='email')

You can then call the command like this:

UserSignup.run(name="Omar Bohsali", email="me@omarish.com")

Modularity

Say, for example, you need to run this command one-off, or on a batch of users on their behalf. With mutations, you can run the signup command in a regular console, instead of having to sign users up through the web application on their behalf.

Moreover, you can validate against any condition. Any method inside a Mutation class that’s prefixed with validate_ will be treated as a validation method and will be run before the command can be executed. For example:

import mutations

class UserSignup(mutations.Mutation):
    """Sign up the user if and only if their name is a palindrome. """
    name = mutations.fields.CharField()
    email = mutations.fields.CharField()

    def validate_name_is_palindrome(self):
        """
        Since this method is prefixed with validate_, it's assumed to be
        a validator and will be run before the command is executed.
        Validators should either return None (if the validator passes), or
        raise ValidationError with an error message if there is a problem.
        """
        if not is_palindrome(self.name):
            raise mutations.error.ValidationError(f"{self.name} is not a Palindrome!")

    def execute(self):
        """
        This only gets called if the user's name is a palindrome.
        """
        user = User.objects.create([...])

Conclusions

Mutations helps you split out your business logic and run it in separate, reusable, testable command classes. It keeps your models thin, key commands testable, is modular, and extensible. Give it a try — it’s available on GitHub and PyPi, and tweet @omarish with any feedback/suggestions.