Back-end9-minute read

Single Responsibility Principle: A Recipe for Great Code

Maintainable code is something we all desire and there are no shortage of coding principles that promise it. It is not always apparent how tremendously useful these principles are during the early stages of development. Nonetheless, the effort put in to ensure these qualities certainly pay off as the project grows and development continues. In this article, Toptal engineer Adel Fayzrakhmanov discusses how the Single Responsibility Principle is one of the most important aspect in writing good maintainable code.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Maintainable code is something we all desire and there are no shortage of coding principles that promise it. It is not always apparent how tremendously useful these principles are during the early stages of development. Nonetheless, the effort put in to ensure these qualities certainly pay off as the project grows and development continues. In this article, Toptal engineer Adel Fayzrakhmanov discusses how the Single Responsibility Principle is one of the most important aspect in writing good maintainable code.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Adel Fayzrakhmanov
Verified Expert in Engineering

Adel (MCE) has 15+ years in software development, focused on web technology and quality architecture. PHP and .NET are his forté.

Previously At

Cornell University
Share

Regardless of what we consider to be great code, it always requires one simple quality: the code must be maintainable. Proper indentation, neat variable names, 100% test coverage, and so on can only take you so far. Any code which is not maintainable and cannot adapt to changing requirements with relative ease is code just waiting to become obsolete. We may not need to write great code when we are trying to build a prototype, a proof of concept or a minimum viable product, but in all other cases we should always write code that is maintainable. This is something that should be considered a fundamental quality of software engineering and design.

Single Responsibility Principle: A Recipe for Great Code

In this article, I will discuss how the Single Responsibility Principle and some techniques that revolve around it can give your code this very quality. Writing great code is an art, but some principles can always help give your development work the direction it needs to head towards to produce robust and maintainable software.

Model Is Everything

Almost every book about some new MVC (MVP, MVVM, or other M**) framework is littered with examples of bad code. These examples try to show what the framework has to offer. But they also end up providing bad advice for beginners. Examples like “let’s say we have this ORM X for our models, templating engine Y for our views and we will have controllers to manage it all” achieve nothing other than humongous controllers.

Although in defense of these books, the examples are meant to demonstrate the ease at which you can get started with their framework. They are not meant to teach software design. But readers following these examples realize, only after years, how counterproductive it is to have monolithic chunks of code in their project.

Models are the heart of your app.

Models are the heart of your app. If you have models separated from the rest of your application logic, maintenance will be much easier, regardless of how complicated your application becomes. Even for complicated applications, good model implementation can result in extremely expressive code. And to achieve that, start by making sure that your models do only what they are meant to do, and don’t concern themselves with what the app built around it does. Furthermore, it doesn’t concern itself with what the underlying data storage layer is: does your app rely on an SQL database, or does it store everything in text files?

As we continue this article, you will realize how great code is a lot about separation of concern.

Single Responsibility Principle

You probably have heard about SOLID principles: single responsibility, open-closed, liskov substitution, interface segregation and dependency inversion. The first letter, S, represents Single Responsibility Principle (SRP) and its importance cannot be overstated. I would even argue that it is a necessary and sufficient condition for good code. In fact, in any code that is badly written, you can always find a class that has more than one responsibility - form1.cs or index.php containing a few thousand lines of code is not something that rare to come by and all of us probably have seen or done it.

Let’s take a look at an example in C# (ASP.NET MVC and Entity framework). Even if you are not a C# developer, with some OOP experience you will be able to follow along easily.

public class OrderController
{
...

    	public ActionResult CreateForm()
    	{
        	/*
        	* View data preparations
        	*/

        	return View();
    	}

    	[HttpPost]
    	public ActionResult Create(OrderCreateRequest request)
    	{
        	if (!ModelState.IsValid)
        	{
            	/*
             	* View data preparations
            	*/

            	return View();
        	}

        	using (var context = new DataContext())
        	{
                   var order = new Order();
                    // Create order from request
                    context.Orders.Add(order);

                    // Reserve ordered goods
                    …(Huge logic here)...

                   context.SaveChanges();

                   //Send email with order details for customer
        	}

        	return RedirectToAction("Index");
    	}

... (many more methods like Create here)
}

This is a usual OrderController class, its Create method shown. In controllers like this, I often see cases where the Order class itself is used as a request parameter. But I prefer to use special request classes. Again, SRP!

Too many jobs for a single controller

Notice in the snippet of code above how the controller knows too much about “placing an order”, including but not limited to storing the Order object, sendings emails, etc. That is simply too many jobs for a single class. For every little change, the developer needs to change the entire controller’s code. And just in case another Controller also needs to create orders, more often than not, developers will resort to copy-pasting the code. Controllers should only control the overall process, and not actually house every bit of logic of the process.

But today is the day we stop writing these humongous controllers!

Let us first extract all business logic from the controller and move it to a OrderService class:

public class OrderService
{
    public void Create(OrderCreateRequest request)
    {
        // all actions for order creating here
    }
}

public class OrderController
{
    public OrderController()
    {
        this.service = new OrderService();
    }
    
    [HttpPost]
    public ActionResult Create(OrderCreateRequest request)
    {
        if (!ModelState.IsValid)
        {
            /*
             * View data preparations
            */

            return View();
        }

        this.service.Create(request);

        return RedirectToAction("Index");
   }

With this done, the controller now only does only what it is intended to do: control the process. It knows only about views, OrderService and OrderRequest classes - the least set of information required for it to do its job, which is managing requests and sending responses.

This way you will rarely change controller code. Other components such as views, request objects and services can still change as they are linked to business requirements, but not controllers.

This is what SRP is about, and there are many techniques for writing code that meets this principle. One example of this is dependency injection (something that is also useful for writing testable code).

Dependency Injection

It is hard to imagine a large project based on Single Responsibility Principle without Dependency Injection. Let us take a look at our OrderService class again:

public class OrderService
{
   public void Create(...)
   {
       // Creating the order(and let’s forget about reserving here, it’s not important for following examples)
       
       // Sending an email to client with order details
       var smtp = new SMTP();
       // Setting smtp.Host, UserName, Password and other parameters
       smtp.Send();
   }
}

This code works, but isn’t quite ideal. To understand how the create method OrderService class works, they are forced to understand the intricacies of SMTP. And, again, copy-paste is the only way out to replicate this use of SMTP wherever it is needed. But with a little refactoring, that can change:

public class OrderService
{
    private SmtpMailer mailer;
    public OrderService()
    {
        this.mailer = new SmtpMailer();
    }

    public void Create(...)
    {
        // Creating the order
        
        // Sending an email to client with order details
        this.mailer.Send(...);
    }
}

public class SmtpMailer
{
    public void Send(string to, string subject, string body)
    {
        // SMTP stuff will be only here
    }
}

Much better already! But, OrderService class still knows a lot about sending email. It needs exactly SmtpMailer class to send email. What if we want to change it in the future? What if we want to print the contents of the email being sent to a special log file instead of actually sending them in our development environment? What if we want to unit test our OrderService class? Let us continue with refactoring by creating an interface IMailer:

public interface IMailer
{
    void Send(string to, string subject, string body);
}

SmtpMailer will implement this interface. Also, our application will use an IoC-container and we can configure it so that IMailer is implemented by SmtpMailer class. OrderService can then be changed as follows:

public sealed class OrderService: IOrderService
{
    private IOrderRepository repository;
    private IMailer mailer;
    public OrderService(IOrderRepository repository, IMailer mailer)
    {
        this.repository = repository;
        this.mailer = mailer;
    }

    public void Create(...)
    {
        var order = new Order();
        // fill the Order entity using the full power of our Business Logic(discounts, promotions, etc.)
        this.repository.Save(order);

        this.mailer.Send(<orders user email>, <subject>, <body with order details>);
    }
}

Now we are getting somewhere! I took this chance to also make another change. The OrderService now relies on IOrderRepository interface to interact with the component that stores all our orders. It no longer cares about how that interface is implemented and what storage technology is powering it. Now OrderService class has only code that deals with order business logic.

This way, if a tester were to find something behaving incorrectly with sending emails, developer knows exactly where to look: SmtpMailer class. If something was wrong with discounts, developer, again, knows where to look: OrderService (or in case you have embraced SRP by heart, then it may be DiscountService) class code.

Event Driven Architecture

However, I still don’t like the OrderService.Create method:

    public void Create(...)
    {
        var order = new Order();
        ...
        this.repository.Save(order);

        this.mailer.Send(<orders user email>, <subject>, <body with order details>);
    }

Sending an email isn’t quite a part of the main order creation flow. Even if the app fails to send the email, the order is still created correctly. Also, imagine a situation where you have to add a new option in the user settings area that allows them to opt-out from receiving an email after placing an order successfully. To incorporate this into our OrderService class, we will need to introduce a dependency, IUserParametersService. Add localization into the mix, and you have yet another dependency, ITranslator (to produce correct email messages in the user’s language of choice). Several of these actions are unnecessary, especially the idea of adding these many dependencies and ending up with a constructor that does not fit on the screen. I found a great example of this in Magento’s codebase (a popular ecommerce CMS written in PHP) in a class that has 32 dependencies!

A constructor that does not fit on the screen

Sometimes it is just hard to figure out how to separate this logic, and Magento’s class is probably a victim of one of those cases. That is why I like the event-driven way:

namespace <base namespace>.Events
{
[Serializable]
public class OrderCreated
{
    private readonly Order order;

    public OrderCreated(Order order)
    {
        this.order = order;
    }

    public Order GetOrder()
    {
        return this.order;
    }
}
}

Whenever an order is created, instead of sending an email directly from the OrderService class, special event class OrderCreated is created and an event is generated. Somewhere in the application event handlers will be configured. One of them will send an email to the client.

namespace <base namespace>.EventHandlers
{
public class OrderCreatedEmailSender : IEventHandler<OrderCreated>
{
    public OrderCreatedEmailSender(IMailer, IUserParametersService, ITranslator)
    {
        // this class depend on all stuff which it need to send an email.
    }

    public void Handle(OrderCreated event)
    {
        this.mailer.Send(...);
    }
}
}

The class OrderCreated is marked as Serializable on purpose. We can handle this event immediately, or store it serialized in a queue (Redis, ActiveMQ or something else) and process it in a process/thread separate from the one handling web requests. In this article the author explains in detail what event-driven architecture is (please pay no attention to the business logic within the OrderController).

Some may argue that it is now difficult to understand what is going on when you create the order. But that cannot be any further from the truth. If you feel that way, simply take advantage of your IDE’s functionality. By finding all the usages of OrderCreated class in the IDE, we can see all the actions associated with the event.

But when should I use Dependency Injection and when should I use an Event-driven approach? It is not always easy to answer this question, but one simple rule that may help you is to use Dependency Injection for all your main activities within the application, and Event-driven approach for all secondary actions. For example, use Dependecy Injection with things like creating an order within the OrderService class with IOrderRepository, and delegate sending of email, something that is not a crucial part of the main order creation flow, to some event handler.

Conclusion

We started off with a very heavy controller, just one class, and ended up with an elaborate collection of classes. The advantages of these changes are quite apparent from the examples. However, there are still many ways to improve these examples. For example, OrderService.Create method can be moved to a class of its own: OrderCreator. Since order creation is an independent unit of business logic following Single Responsibility Principle, it is only natural for it to have its own class with its own set of dependencies. Likewise, order removal and order cancellation can each be implemented in their own classes.

When I wrote highly coupled code, something similar to the very first example in this article, any small change to requirement could easily lead to many changes in other parts of code. SRP helps developers write code that are decoupled, where each class has its own job. If specifications of this job changes, developer makes changes to that specific class only. The change is less likely to break the entire application as other classes should still be doing their job as before, unless of course they were broken in the first place.

Developing code upfront using these techniques and following Single Responsibility Principle can seem like a daunting task, but the efforts will certainly pay off as the project grows and the development continues.

Hire a Toptal expert on this topic.
Hire Now
Adel Fayzrakhmanov

Adel Fayzrakhmanov

Verified Expert in Engineering

Kazan, Tatarstan, Russia

Member since October 19, 2015

About the author

Adel (MCE) has 15+ years in software development, focused on web technology and quality architecture. PHP and .NET are his forté.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

PREVIOUSLY AT

Cornell University

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

Join the Toptal® community.