What Does SRP Try To Solve?

Before I answer that, lets define what a class is. A class is essentially a blueprint or template for creating objects. An object is an instance of a class. The class defines the properties (attributes) and methods (functions) that the objects created from it will have.

If a class handles more than one concern, like managing user data, sending emails, and logging activity, then it’s a "God class" and could be a disaster waiting to happen.

Why are “God classes” bad? Well, they can be hard to:

  • Understand.

  • Maintain.

  • Test.

  • Extend.

  • Refactor.

  • Reuse.

This principle aims to avoid potential a “God class”. A “God class” is the equivalent of a jack-of-all-trades, trying to do everything and, in the process, doing nothing particularly well.

The Definition

We know what SRP tries to prevent, but what is it?

“A class should have only one reason to change.”

It sounds simple enough, right? But is it? What is a reason to change? What does that even mean? How can you possibly define a reason for change?

You’re absolutely right to be skeptical—this principle does sound simple on the surface, but it’s anything but straightforward once you dig in.

If you wanted to abuse this principle, you could define a very high-level responsibility like Checkout—and stuff everything into that class: payment, taxes, validation, email confirmation, user authentication, and so on. This sounds like it would satisfy the "one reason to change" rule, because everything is about checkout, right?

This would be an over-simplification of the principle. It’s not enough to say “checkout” is one reason—you need to define the responsibilities more clearly. “Checkout” is still too vague. Each specific responsibility within that process (e.g., payment processing, tax calculation, or order confirmation) should have its own class with one well-defined responsibility. This makes your system more modular, maintainable, and adaptable to change.

In practical terms, a “reason to change” is something that would prompt a developer to alter the class, whether due to a new business requirement, a bug fix, or a performance improvement.

When we talk about a class having only one reason to change, we're really saying that if you make a change for one reason, it shouldn't affect other responsibilities or aspects of the class.

Defining A Reason For Change

The trick is finding the right balance between too much and too little. Defining the right responsibility for each class is a skill that comes with practice and understanding your system’s needs. When done well, SRP leads to:

  • Better maintainability: You can make changes without affecting unrelated parts of the system.

  • Easier testing: Smaller classes focused on a single responsibility are easier to test.

  • Simpler code: The code becomes more understandable and decoupled, reducing the cognitive load for developers.

To help you define clear responsibilities and apply the Single Responsibility Principle effectively, here’s a few tips:

  • Define meaningful boundaries: When identifying responsibilities, think about what might change independently from other concerns. If one part of a class is likely to change for one reason (e.g., tax calculation), and another part is likely to change for a different reason (e.g., payment processing), then those should be split into separate classes.

  • Don't over-engineer: Avoid going too granular. Sometimes, a single class with a couple of distinct methods is perfectly acceptable, as long as the methods are logically tied together and serve a common purpose.

  • Start broad, then refine: In complex systems, it can be useful to start with broader classes and then refactor as you gain more clarity on how different parts of the system will evolve. Don’t be afraid to revisit your class boundaries as the system grows and requirements change.

  • Communicate with your team: SRP isn’t a rigid rule—it’s a principle. Make sure you and your team align on what constitutes a responsibility, and remember that it may look different depending on your project’s complexity and needs.

It’s also worth mentioning that SRP done correctly is NOT over-engineering. I see people roasting SRP, and it’s often because they’ve encountered poor implementations of the principle—where the focus was on rigidly breaking down responsibilities at the cost of readability and practicality.

The difficulty comes in defining that responsibility—too broad, and you risk violating the principle; too narrow, and you might end up with a lot of unnecessary classes and complexity.

Real-World Example

Let’s consider an e-commerce site:

  • You have a Checkout class, which is in charge of processing orders.

  • However, let’s say you need to introduce a new payment gateway or change the way discounts are applied. If all of these responsibilities are stuffed into a single class, every time a change happens, you might need to modify different parts of the class that have nothing to do with the change at hand.

class Checkout
{
    public function processOrder(Order $order): void
    {
        // Apply discount
        $discount = 0.10;
        $total = $order->getTotal();
        $totalAfterDiscount = $total - ($total * $discount);
        // Code here integrating with multiple payment gateways
        // Code here to send confirmation email
    }
}

Instead, break it down like this:

  • PaymentProcessor class for handling payment logic.

  • DiscountCalculator class for managing discounts.

  • OrderConfirmation class for confirming the order and sending emails.

class Checkout {     
    public function __construct(        
        protected PaymentProcessor $paymentProcessor,        
        protected DiscountCalculator $discountCalculator,
        protected OrderConfirmation $orderConfirmation
    ) {  

    }     

    public function processOrder(Order $order) {         
        // Apply discount         
        $totalAfterDiscount = $this->discountCalculator->apply($order);
        // Process payment         
        $this->paymentProcessor->process($totalAfterDiscount);        
        // Confirm order         
        $this->orderConfirmation->send($order);
    } 
}

class DiscountCalculator
{
    public function apply(Order $order): void
    {
        // Implement payment logic here
    }
}

class PaymentProcessor
{
    public function process(float $amount): void
    {
        // Implement payment logic here
    }
}

class OrderConfirmation
{
    public function send(Order $order): void
    {
        // Implement payment logic here
    }
}

Now, if you change how payments work (e.g., adding a new payment gateway), you only need to touch the PaymentProcessor class and the Checkout class itself.

But why is that important?

  • Reduce risk by limiting the impact of changes to just the relevant areas.

  • Improve maintainability by making each class easier to understand, test, and modify.

  • Enhance flexibility and scalability by making it easier to add new payment methods or change other components of the system without touching unrelated parts.

  • Support reusability because the PaymentProcessor class can be reused in other contexts.

  • Make your code cleaner and easier to work with by adhering to the principle that classes should only have one reason to change, avoiding unnecessary complexity.

Final Thoughts

SRP isn’t just a principle you follow once and forget; it’s a skill that develops with experience. You’ll get better at identifying the right level of responsibility through trial, error, and practice—there’s no shortcut to mastering it.

Think of SRP like learning an instrument. At first, you’ll hit a few wrong notes or overcomplicate things, but over time, you’ll find the rhythm, striking the balance between clarity and simplicity without over-engineering.

So, if you’re struggling to figure out the right boundaries in your classes, don’t worry—that’s part of the process. You’ll refine your understanding as you go.

Here’s my advice: go practice! Try applying SRP, notice where it makes a difference, and recognise the challenges—it’s all part of the learning process. Over time, SRP will become second nature, elevating the quality of your code almost effortlessly.

One thing that really helped me was making sure each class and it’s methods could be properly tested. When you prioritise testability, you’re naturally encouraged to break things down just enough.

Well done if you stayed till the end…

The Grumpy Dev is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.