Business Logic in Django projects
So now you have an incredible idea and you want to make it reality using Django. In order to learn what to do you search through the internet and find a large variety of articles and tutorials teaching you how to build your Models, Views, and Templates.
As your project grows you realize that specific flows start to appear. A validation for a money transaction, a flow that will have three other side-effects, or a simply email you have to send every time something happens.
Like the ugly duckling, your business logic has nowhere to go. The articles have only simple examples for small applications and you find yourself diverging on whether put that logic on your Model, View, Form, or, why not, your template :( .
I want to help you with that. After many of those projects I've learned from the hard way what doesn't work when you scale to large Django projects.
Before we start let me be clear about one thing. Ideally you wouldn't think on which framework or "infrastructure" to use until you actually build your application. Software architecture "heroes" such as Martin Fowler, Eric Evans, and Robert C. Martin already talked about it on their books.
However, I understand the agility a framework can give you when you're testing an idea, so I want to give you some tips to make your project cleaner and easier to maintain.
Let me define the goal of each layer on the MVT architecture that Django use.
M — Model: To me, the goal of the Models is to handle the data. That means, it should be responsible for maintaining the integrity of the state of the object, and implement the behaviors that this object may have. That means, it is also responsible for changing the state of the object. And that's it! No side-effects! I've already discussed about it here.
V — View: The view has few goals to me. First, it is responsible for getting the inputs that come via HTTP request. Secondly, it should sending this to the process that will process that data and collect the output of the operation. And thirdly, it is responsible for processing the template and send the response back to client.
The view is not responsible for having the process itself, it is the controller. No business logic should live here. In fact, it should be the thinest layer on your application.
T — Template: The template is there to define the markup that will be processed by the view. The template system that Django uses is sophisticated and allow extensions and few logical operations. Due to the high-level of customization that is possible here, it is very easy to add a lot of business logic to that. Avoid it! Leave your template with the responsibility of dealing only with the presentation aspect of your application.
Now, I understand that you may need to have some conditions to expose a piece of your html. I believe it is ok if that is not all over the place and NOT too much complex. Don’t call methods directly on your templates, it becomes really hard to debug. If you really need to have a complex logic, break down your templates into as many templates as you need to support the different cases, and let your view decide which one to render. Use the power of the template system to extend templates and save some lines of code.
Ok Jair, if none of those layers should carry my business logic, where should I put it?
A lot of the articles on the internet answer this question introducing the Form layer. However, I think about Forms more as validators of the input that my user is adding on my application.
Validations such as the formatting the date field, make sure an integer number is bigger than zero, and require all fields to be filled up, live in the Form. It doesn't care if the username you're trying to add already exists. It is not the Form's responsibility to know about business logic.
The right answer for that question is to introduce another layer.
Robert C. Martin talks about a layer called UseCase on his book Clean Architecture. Eric Evans calls it as Service layer on his book Domain-Driven design. Martin Fowler calls it Service layer as well, but sometimes he refers to a similar layer, the Controller layer.
I personally like the UseCases concept and for the purpose of this article we will referrer to it as the U on a MUVT architecture.
So let's define the goal of it.
U — Usecase: The goal of an use case is to concentrate the business logic for the operations of an application. It knows all Models that should be part of the flow and knows the API of those models. It also orchestrate all the side-effects and therefore can make the use of other use cases.
Adding this layer to your application will make your process easy to test, since you don't need to spin up an entire request to test the flow, and allow you to mock the other layers. Thus, you can test the logic individually.
So, let's imagine a process to registers new users. Whenever a new user register herself, we need to check whether the username already exists or not. In case it doesn't, the process should send a Welcome email and persist the new user. If it already exists, the application should raise an error.
A simple model for UserAccount could be something like this.
Adding test for this custom manager (Assuming we have installed pytest, pytest-mock, and django-mock-queries).
Now, let's create a class that will send the Welcome email.
Adding tests.
Building the use case now.
Adding tests.
Uhu! We've created and tested the whole "business logic" of our application! And we didn't have to implement the view yet. We're are able to validate our flow, run tests against it, and reuse this whenever we want.
However, let's keep moving down the path and implement the rest of the layers.
I will start with the RegisterAccountForm.
Don't forget the tests.
And finally, the view!
As we can see, views take care of the input and delegate the data process. Collect the output and return it to client.
Testing the view.
Note that it doesn't test the logic here, neither if the Welcome email got sent or not. Also, there's no mention to the UserAccount model. The view doesn't even know that it exists. Therefore, we only need to mock the execute method of the use case.
Wow, it was a quite long journey until here, wasn't it? I want to do a quick recap.
- Models handle data, guarantee integrity, and contain the behaviors that change their state.
- Use cases execute the business logic and run the side effects.
- Views receive the input, delegate the process, return the output.
- Templates deal only with the markup that will be processed by the view.
That's how you start to decouple your application and make flows more reusable, testable, and reliable.
Thanks for reading until here, it was quite a long post!