Implementing a lasting service pattern for your business logic

Best practices for building an understandable, maintainable and scalable home for your custom logic

Ronny Vedrilla
ambient-digital
10 min readMay 27, 2022

--

Photo by Mike Hindle on Unsplash

Motivation

If you need to implement custom functionality or business logic, you usually will implement a service class. A service is a simple class which contains your code in a properly-structured way. In the following, we’d like to present some best practices and do’s and don’ts.

The goal of the following “rules” are to increase readability and the ability to test your code properly. Furthermore, reduce the complexity as much as possible and make it easier for the next developer to change parts of it.

Disclaimer

The following rules or suggestions are outlined with some pieces of code. These snippets are written in Python living in a Django environment. But rest assured, most of these concepts are language-independent and can be implemented in any language. If something is Python-specific, I’ll mention this in the according paragraph.

Please note that for the sake of simplicity, I tried to create simple examples without too much context required. This means that some of them — in a semantic way — are not the best way to implement this piece of logic. They are just examples for the points I want to stress.

Service pattern

Using a service pattern — especially in Django — has been heavily disputed in the community for the last years. In my opinion, this is the result of a very broad definition of what a service really is and what its purpose is supposed to be.

In my case, a service is a class encapsulating a certain piece of (business) logic with the goal of providing the best possible readability and maintainability. Furthermore, if you stick to my suggestions, the implementation might be so clean and slim that by having a look at the public “process” method, where your business logic should be outlined on a somehow abstract layer, you could even discuss this with a domain expert who might not have any programming know-how at all.

It is NOT an abstraction layer to be able to — in theory — easily replace the underlying framework or completely separate the ORM from the business logic. I honestly believe that these approaches focus on the wrong aspects — at least in most cases in web development.

Best practices

Public “process” method

Create a public method “process” which is the single entry-point to this service. This method may take a couple of arguments. Furthermore, this method should — in theory — do nothing but call internal methods. This way, a new developer can easily spot what this service is doing and what steps are required for the intended output. In practice, if you loop over a dataset or do any other trivial operation… well, just do it. Readability is the highest value here.

Good: Example implementation of a process method fetching an order, calculating the shipping costs and informing the customer about the successful purchase

Mark internal methods as “protected”

If you mark your non-public methods as such, you show the next developer how to interact with your service. This increases readability and ease of use. Keep in mind that in some programming languages you cannot access protected methods. This sounds logical, but if you want to write proper unit-tests, you have to have access to the internal methods to structure your tests in a sensible way.

Method “calculate_shipping_costs” marked as “internal”
Good: Method “calculate_shipping_costs” marked as “internal”

Keep the context outside

Try to keep the context outside of your service. For example, a request does not belong in your service, the request user might. This way, it will be easier to test your code and you have more control over what’s going on inside of it.

Good: Pass the user to the service, not the whole request

Moreover, your service class will be more reliable to changes of the surrounding framework because it only takes — in this case — an explicit user and doesn’t try to pick out the user from a request object which might change in the future.

Bad: Pass the whole request and extract later what you need

Pass required parameters via the constructor

When you need to pass a couple of variables to your service, try to do this in the constructor rather than the process-method. This way, you keep your process clean(-er) and inside the constructor, you add these variables to the class-scope to make them class-wide accessible. If you allow users to pass variables at any point except the constructor — or maybe the process method — the code will be hard to understand and it won’t be clear what this service needs to be executed.

Good: Example of passing all required parameters in the constructor and setting them to the class-scope

Pro-Tip for python: By adding an asterisk as the “first” parameter, you’ll enforce keyword-arguments and prohibit positional arguments. This has two huge benefits:

  1. You cannot pass variable “a” on the position where “b” is supposed to be.
  2. If you have to refactor your service at some point, you can easily add or remove variables and don’t have to watch out for problems caused by the implicit way of passing the variables: “Explicit is better than implicit”

Limit write access to class attributes

Try (hard) not to write any class attributes/variables except for the constructor and process methods. Your code will get way harder to understand if any (sub-)method can alter your variables.

Avoid class attributes if possible

When creating a class attribute/variable, you put it in the (class-wide) global scope. It is way harder to understand and follow your data when processing the service when you have to watch out for side-effects from any part of your service instead of just look at the in- and output of your method. This being said, of course you should not NOT use class attributes at all. Just keep in mind that if you can avoid them, do so.

Good: Here you can see that the shipping costs and the order are set only in the methods and not in the class-wide scope

Try to avoid optional parameters

Keep the KISS principle (Keep it simple, stupid) in mind. Optional parameters will lead to more possible outcomes when executing your service. More outcome means more complexity which will result in more potential bugs and more complicated unit tests. Always try to work with the most stable setup as possible. So give it a good thought before making a parameter optional! In a perfect world, your process method doesn’t take any parameters. Usually, an optional parameter is an indicator of two different (business) cases. It might be a way to create two classes and use inheritance to get rid of the optional parameter.

Try to avoid flags

Flags tend to make a service bulky. Imagine, you have a service processing a shipment. The customer can be a regular customer, but if the customer is also a merchant, then something else happens. Developers might be tempted to pass a flag to the service, telling it which case is going to be calculated: customer or merchant. The problem: You have to be very careful that you cover all your cases properly because you’ll end up with lots of if-else code branches. Inheriting from a base service and overwriting the methods that differ might be a more elegant way. This being said, special cases are always hard to handle, and you will have to make trade-offs in the end.

Bad: Bulky and unnecessarily complex method to calculate the order price

Limit your services depth

When trying to encapsulate the service code properly, the eager developer is tempted to create many small methods which will call themselves and therefore increase the depth of the service. Try to stay on one level if possible and if not, actively work to reduce the number of steps your data will flow down your services methods. Every story the data descents adds more complexity to your service and makes it exponentially harder to understand.

Bad: This shows a depth of 4. In this case, the example is simple and straightforward but if you add complexity, this can become very obscure.

Use docstrings

Do yourself and the next developer (probably you again anyways) a favour and write docstrings! One for the class and one for every method. The docstring should contain one or two sentences on an abstract level about what is going to happen inside this class or method. Don’t go into technical detail, they are supposed to help understand the next person what you implemented — not how.

Good: Example docstring for a method explaining what is happening — not how it’s implemented.

Use inline comments

Though this topic is a little bit religious and disputed, I strongly recommend using inline comments. They structure your code and help the reader follow your thoughts. If my method contains loops or complicated logic, even trivial comments like “iterate over all objects having X” will be a great benefit for the overall readability.

Good: Inline comments explaining every step of the algorithm

Explain yourself

Sometimes you have to build something a little bit fishy. Or you have a great but not so obvious idea. Please, do everyone a favour and write a line of comment next to it! Custom functionality and business logic will be the hardest part of your application to maintain and understand. If something breaks, usually it comes with a huge pain and lots of — often monetary — consequences for the customer. So on a necessary refactoring, the future developers will be very reluctant to touch this odd and uncommented piece of code. If it is obscure, the chance that it will not undergo required refactorings, will increase by a million.

Use mixins for inheritance

Inheritance is a great way of structuring your code and avoid unnecessary code duplication. Keep in mind, that you don’t always have to create a whole class. Often it is easier, more readable and more reusable if you just create a small mixin containing a handful or even just one method. This mixin then can be included in any number of service classes.

Good: Encapsulate logic that you need in other services in a mixin to stay DRY and readable

Keep your methods short

Try to keep your service methods below 50 LOC to ensure good readability. Of course, sometimes breaking up a method into multiple smaller ones will decrease readability. So keep in mind, that you tend to write short methods and encapsulate what is possible and sensible. Implementing custom logic is never trivial, so unfortunately you have to think about, what you are doing.

Use type-hinting

It’s really fast to do and at least for trivial data types like string, integers and floats — do yourself a favour and type-hint what’s possible. It will give the reader more context on what is going on and he/she might be quicker in spotting this one annoying bug — especially because your favourite IDE will point out mistakes for you. This being said, if your language doesn’t support this, well, then it’s not going to happen for you.

Good: Every parameter and the return value are defined. Works usually without a hassle for “simple” types.

DRY is about knowledge, not code

Be careful when implementing business logic and put together code for multiple cases. Being DRY is not about code, it’s about (business) knowledge. So be sure only to merge logic that represents the same business logic.

Have a look at this example. The query takes two dates, both are the day exactly one month ago. But date 1 tells the query how new an employee is allowed to be and the second one determines how much time has to lie between two invitations. Even though it is both one month, it would be a mistake to put both in the same variable because the semantic is completely different.

Both variables are (currently) calculating the same date but they are semantically different

You can read it up here: https://verraes.net/2014/08/dry-is-about-knowledge/

Alternative strategies

Keep in mind that when implementing business flows, a finite state machine (FSM) can come in very handy. It provides a well-known and easy-to-grasp pattern. When working with Django, you can get first impressions on this topic in the django-viewflow package documentation.

Example finite state machine of a student paper approval flow (Source: https://youtu.be/nG_ZsNxRz0o)

Conclusion

If you managed to reach this point, you should have gotten some battle-tested ideas on how to properly structure your business logic within your application. I think it’s safe to say that most — if not all — of these concepts are easy to implement and to understand. But as always, if you have a good reason to break one of those rules: Do it!

I hope that I might be able to improve your life as a developer slightly. If you have more or better ideas or are under the impression that I’m talking bullocks, I’d be happy to discuss this with you.

--

--

Ronny Vedrilla
ambient-digital

Tech Evangelist and Senior Developer at Ambient in Cologne, Germany.