Skip to main content
Articles/architecture/single-responsibility-principle
Series: Low-Level Design Interview Prepโ€ขPart 1 of 17
Scenario Article

Simplifying Code with the Single Responsibility Principle

The Filing Cabinet That Also Sends Emails

Abstract Algorithmsโ€ขMar 9, 2026
XLinkedInReddit
๐Ÿ’ก Executive Summary

Imagine a filing clerk whose job is: 1.

๐ŸŽฏ System Scenario & Requirements

System Goal

Imagine a filing clerk whose job is: 1.

Actors & Components

  • Application or API tier
  • Core service under design
  • Operator or SRE observer

Functional Requirements

  • Imagine a filing clerk whose job is: 1.
  • Store documents (filing cabinet logic).
  • Send notification emails whenever a document is filed.

Constraints & Assumptions

  • SRP is sometimes misunderstood as "one method per class." That's wrong.
  • Cohesion is the right mental model: group methods that change together and depend on the same data .

โšก Execution Stages & Decision Trace

1

Stage 1 โ€” The Classic Violation: User Manager Who Does Everything

Action / Decision:

Reasons this class must change: - The database schema changes โ†’ modify DB logic.

Component Behavior:

- The welcome email template changes โ†’ modify email logic.

Observable Output: - The audit log format changes โ†’ modify logging logic.
Read Full Stage in Deep Dive โ†“
๐Ÿ“–

Deep Dive

Detailed Architectural Analysis

Scenario context

Imagine a filing clerk whose job is:

  1. Store documents (filing cabinet logic).
  2. Send notification emails whenever a document is filed.
  3. Log activity to an audit trail.

One person doing three jobs โ€” fine until they're on vacation and you only need the email changed. You have to find and understand the whole multi-responsibility class just to change one notification template.

SRP says: each clasโ€ฆ

This walkthrough focuses on The Filing Cabinet That Also Sends Emails.

Before making a design choice, clarify:

  • Imagine a filing clerk whose job is: 1.
  • Store documents (filing cabinet logic).
  • Send notification emails whenever a document is filed.

Concepts in play: design-principles-solid, event-driven-architecture, jvm-internals-and-garbage-collection, low-level-design-systems

Stage 1 โ€” The Classic Violation: User Manager Who Does Everything

  • Reasons this class must change: - The database schema changes โ†’ modify DB logic.
    • The welcome email template changes โ†’ modify email logic.
    • The audit log format changes โ†’ modify logging logic.
  • Three reasons to change = three responsibilities.
// โŒ SRP violation โ€” two very different reasons to change this class
class UserManager {
    public void saveUser(User user) {
        // 1. save to DB
        db.execute("INSERT INTO users VALUES (?)", user.data());
        // 2. send a welcome email
        emailService.send(user.email, "Welcome to our platform!", WELCOME_TEMPLATE);
        // 3. write audit log
        logger.info("User created: " + user.getId());
    }
}
classDiagram
    class UserManager {
        +saveUser()
        +sendWelcomeEmail()
        +logAuditTrail()
    }
    note for UserManager "Violates SRP: DB, email, and logging in one class"

Observable outcome: Three reasons to change = three responsibilities.

Stage 2 โ€” The SRP Fix: One Class, One Job

  • Now each class has exactly one reason to change.
  • UserRegistrationService orchestrates the flow but owns none of the individual mechanics.
  • Notice the key benefit beyond cleanliness: independent deployability of understanding .
  • A new developer joining the team can read WelcomeEmailService in thirty seconds and fully understand what it does and what could cause it to change.
// โœ… Split into single-responsibility classes
class UserRepository {
    public void save(User user) {
        db.execute("INSERT INTO users VALUES (?)", user.data());
    }
}

class WelcomeEmailService {
    public void sendWelcome(User user) {
        emailService.send(user.email, "Welcome!", WELCOME_TEMPLATE);
    }
}

class UserAuditLogger {
    public void logCreated(User user) {
        logger.info("User created: " + user.getId());
    }
}

// Orchestrator โ€” knows when to call each, but not how they work
class UserRegistrationService {
    private final UserRepository repo;
    private final WelcomeEmailService emailSvc;
    private final UserAuditLogger audit;

    public void register(User user) {
        repo.save(user);
        emailSvc.sendWelcome(user);
        audit.logCreated(user);
    }
}
classDiagram
    class UserRegistrationService {
        +register()
    }
    class UserRepository {
        +save()
    }
    class WelcomeEmailService {
        +sendWelcome()
    }
    class UserAuditLogger {
        +logCreated()
    }
    UserRegistrationService --> UserRepository
    UserRegistrationService --> WelcomeEmailService
    UserRegistrationService --> UserAuditLogger

Observable outcome: A new developer joining the team can read WelcomeEmailService in thirty seconds and fully understand what it does and what could cause it to change.

Stage 3 โ€” Deep Dive: Detecting SRP Violations

Observable outcome: You can explain how โ€œDeep Dive: Detecting SRP Violationsโ€ changes the architecture or implementation path.

Stage 4 โ€” SRP Responsibility Flow

  • When a request hits UserRegistrationService.register() , responsibility flows cleanly to dedicated single-purpose classes โ€” each is an isolated change axis.
  • If the database schema changes, only UserRepository is modified.
  • If the email template changes, only WelcomeEmailService is touched.
  • The orchestrator UserRegistrationService never changes for infrastructure reasons โ€” only if the registration workflow itself changes.
flowchart LR
    A["register(user)"] --> B[UserRepository.save]
    A --> C[WelcomeEmailService.sendWelcome]
    A --> D[UserAuditLogger.logCreated]
    B --> E[(Database)]
    C --> F[Email Provider]
    D --> G[Audit Log]

Observable outcome: The orchestrator UserRegistrationService never changes for infrastructure reasons โ€” only if the registration workflow itself changes.

Stage 5 โ€” Real-World Application: SRP at Scale

  • SRP applies at every scale โ€” class, service, and infrastructure.
  • Where to apply SRP first: Start at the class level.
  • Pain signals: unit tests with 5+ mocks in setUp, class names with "And" or "Manager", files frequently edited by two different teams for unrelated features.
  • At the microservice level, SRP manifests as bounded contexts โ€” each service owns one business capability.

Observable outcome: At the microservice level, SRP manifests as bounded contexts โ€” each service owns one business capability.

Stage 6 โ€” Hands-On: Identify and Refactor SRP Violations

  • Here is a class that violates SRP.
  • Identify the responsibilities, then split them.
  • Five responsibilities โ†’ five focused classes: Orchestrator: OrderProcessingService has one responsibility: orchestrating the order workflow.
  • Each collaborator has one responsibility: doing its specific job.
// ๐Ÿ›‘ How many responsibilities does this class have?
class OrderProcessor {
    public void processOrder(Order order) {
        // Responsibility 1: Validate
        if (order.getItems().isEmpty()) {
            throw new IllegalArgumentException("Order has no items");
        }
        // Responsibility 2: Charge
        stripeClient.charge(order.getCustomerId(), order.getTotal());
        // Responsibility 3: Update inventory
        for (Item item : order.getItems()) {
            inventoryDb.decrementStock(item.getSku(), item.getQuantity());
        }
        // Responsibility 4: Notify
        emailService.send(order.getEmail(), "Order confirmed", buildBody(order));
        // Responsibility 5: Audit
        logger.info("Order processed: " + order.getId());
    }
}
class OrderProcessingService {
    public void processOrder(Order order) {
        validator.validate(order);
        paymentService.charge(order);
        inventoryService.decrementStock(order);
        notificationService.sendConfirmation(order);
        auditLogger.logProcessed(order);
    }
}

Observable outcome: Each collaborator has one responsibility: doing its specific job.

End-to-end outcome

After working through the stages, you should be able to explain:

    • The "And" test always works.
  • If you cannot name a class without "And" or "Manager", it has multiple responsibilities.

Alternatives considered

Decision Guide

Decision Guide

Failure and degraded modes

Trade-offs & Failure Modes: SRP vs. Cohesion

  • Detection: SRP is sometimes misunderstood as "one method per class." That's wrong.
  • User or operator impact: SRP is sometimes misunderstood as "one method per class." That's wrong.
First article in seriesNext Article โ†’

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit