JB's Blog

LifeStyle | Coding | Creativity

Single Responsibility Principle In Software Design

by

in

The Single Responsibility Principle (SRP) is the cornerstone of clean, maintainable, and extensible software systems. It’s the “S” in SOLID, but it stands much taller—it influences every aspect of coding, testing, and architecting good systems.

Over the past week, I’ve immersed myself in SRP. This article brings together conceptual understanding, real-world Java examples, testability impacts, refactoring strategies, and architecture design reflections to master SRP deeply.


🧠 What is the Single Responsibility Principle (SRP)?

“A class should have only one reason to change.” — Robert C. Martin (Uncle Bob)

It’s about separating different responsibilities into different classes so that when one part of your system changes, it doesn’t cause unexpected side effects elsewhere.


🎬 Visual Metaphor: CaféBot

Imagine CaféBot that does:

  • Takes orders
  • Makes coffee
  • Cleans tables
  • Handles billing
  • Runs social media campaigns

If you change how billing works, you risk breaking its cleaning or marketing modules.

✅ Instead, design separate bots for each function. Each bot has one reason to change.

Same with your classes.


🔥 Real-World Code Example: Bloated Service

❌ Violation

public class UserService {
    public void registerUser(User user) {
        if (user.getEmail() == null) throw new RuntimeException("Invalid email");
        user.setStatus("ACTIVE");
        repository.save(user);
        emailSender.sendWelcomeEmail(user.getEmail());
    }
}

Responsibilities blended:

  • Validation
  • Business rules
  • Persistence
  • Notification

✅ SRP Refactor

class UserValidator { void validate(User u) { ... } }
class UserBusinessService { void applyDefaults(User u) { ... } }
class UserNotificationService { void sendWelcomeEmail(String email) { ... } }

class UserRegistrationService {
    void register(User user) {
        validator.validate(user);
        business.applyDefaults(user);
        repository.save(user);
        notifier.sendWelcomeEmail(user.getEmail());
    }
}

Now each class has one reason to change. Modular, testable, safe.


🧪 Unit Testing Benefits from SRP

Without SRP:

  • You test validation, persistence, and notification together.
  • Every test requires heavy mocking.
  • Minor change = many failing tests.

With SRP:

@Test void testValidateUser() { validator.validate(user); }
@Test void testBusinessDefaults() { business.applyDefaults(user); }
@Test void testSendEmail() { notifier.sendWelcomeEmail(email); }

Each class is:

  • Easier to mock
  • Faster to test
  • Less brittle

SRP massively boosts unit testability.


🏗️ SRP in Architecture Design

SRP doesn’t stop at classes. Apply it across layers:

LayerResponsibility
ControllerHandle HTTP/Input transport concerns
ServiceApply domain/business rules
RepositoryPersist/retrieve data
External GatewaysNotify, send, receive events

✅ A layered architecture is a macro-application of SRP.


⚙️ SRP in Modular Monoliths and Microservices

  • Each module should have a focused domain (e.g., Billing, Shipping, Inventory)
  • Each microservice should serve one bounded context
  • Split responsibilities across smaller collaborating services, not one monster service.

🚨 Static Utility Class = SRP Violation

public class Utils {
    public static void validateEmail();
    public static void generateInvoice();
    public static void logRequest();
}

✅ Better:

class EmailValidator { boolean isValid(String email); }
class InvoiceGenerator { Invoice generate(Order order); }
class RequestLogger { void log(Request req); }

Group by responsibility, not by syntactic convenience.


🔬 Detecting SRP Violations

ClueViolation
Class hard to name clearlyMixed responsibilities
Changes affect unrelated areasToo many reasons to change
Needs 3+ mocks in unit testTightly coupled behaviors
Massive God ObjectsResponsibility bloat

✅ If you struggle to summarize a class’s purpose in one sentence, SRP is likely violated.


🛠️ Mini Refactoring Challenge: PaymentProcessor

Before:

public class PaymentProcessor {
    void process(Payment payment) {
        validate(payment);
        charge(payment);
        save(payment);
        notify(payment);
    }
}

After:

class PaymentValidator { void validate(Payment p); }
class PaymentService { void charge(Payment p); }
class PaymentRepository { void save(Payment p); }
class PaymentNotifier { void sendReceipt(Payment p); }

class PaymentProcessor {
    void process(Payment p) {
        validator.validate(p);
        service.charge(p);
        repository.save(p);
        notifier.sendReceipt(p);
    }
}

✅ Easy to test, extend, modify, debug.


🧠 Visual Mind Map

           +------------------------+
           | Single Responsibility |
           +------------------------+
                     |
      +--------------+--------------+
      |              |              |
 Class-level     Method-level     System-level
 Responsibilities    Simplicity    Service Boundaries

⚡ Key Takeaways from Deep Dive

LearningApplication
One reason to change per classReduces coupling, increases clarity
SRP leads to natural testable boundariesBoosts TDD and maintenance agility
SRP scales into microservices and modular monolithsCleaner system growth
Static utils often signal SRP violationsRefactor towards service objects

SRP is foundational not just for coding but for scalable system design.


Leave a comment