🏗️

Single Responsibility Principle (SRP)

Object-Oriented Programming Intermediate 2 min read 300 words

A class should have one, and only one, reason to change

Single Responsibility Principle (SRP)

“A class should have one, and only one, reason to change.” — Robert C. Martin

The Single Responsibility Principle states that a class should be responsible for only one thing. If a class has multiple responsibilities, changes to one responsibility may break the others.

Understanding “Responsibility”

A “responsibility” is a reason to change. Ask yourself:

  • “Who might request changes to this class?”
  • “What different things could cause this class to change?”

If multiple stakeholders or concerns could cause changes, the class has too many responsibilities.

Violation Example

// BAD: Multiple responsibilities in one class
public class UserManager
{
    // Responsibility 1: User management
    public void RegisterUser(string email, string password) { /* ... */ }
    public void LoginUser(string email, string password) { /* ... */ }
    public void UpdateUserProfile(int userId, string name) { /* ... */ }

    // Responsibility 2: Email handling
    public void SendWelcomeEmail(string email) { /* ... */ }
    public void SendPasswordResetEmail(string email) { /* ... */ }

    // Responsibility 3: Database operations
    public void SaveToDatabase(User user) { /* ... */ }
    public void DeleteFromDatabase(int userId) { /* ... */ }

    // Responsibility 4: Validation
    public bool ValidateEmail(string email) { /* ... */ }
    public bool ValidatePassword(string password) { /* ... */ }

    // Responsibility 5: Logging
    public void LogUserActivity(int userId, string activity) { /* ... */ }
}

// Problems:
// - Changes to email service affect this class
// - Changes to database schema affect this class
// - Changes to validation rules affect this class
// - Changes to logging affect this class
// - Hard to test - need to mock everything
// - Hard to reuse - email sending tied to user management

Applying SRP

Split responsibilities into separate, focused classes:

// Responsibility: Validation
public interface IUserValidator
{
    ValidationResult ValidateEmail(string email);
    ValidationResult ValidatePassword(string password);
    ValidationResult ValidateRegistration(string email, string password);
}

public class UserValidator : IUserValidator
{
    public ValidationResult ValidateEmail(string email)
    {
        var errors = new List<string>();

        if (string.IsNullOrWhiteSpace(email))
            errors.Add("Email is required");
        else if (!email.Contains("@") || !email.Contains("."))
            errors.Add("Invalid email format");

        return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
    }

    public ValidationResult ValidatePassword(string password)
    {
        var errors = new List<string>();

        if (string.IsNullOrWhiteSpace(password))
            errors.Add("Password is required");
        if (password?.Length < 8)
            errors.Add("Password must be at least 8 characters");
        if (!password?.Any(char.IsDigit) ?? true)
            errors.Add("Password must contain a digit");

        return new ValidationResult { IsValid = !errors.Any(), Errors = errors };
    }

    public ValidationResult ValidateRegistration(string email, string password)
    {
        var emailResult = ValidateEmail(email);
        var passwordResult = ValidatePassword(password);

        return new ValidationResult
        {
            IsValid = emailResult.IsValid && passwordResult.IsValid,
            Errors = emailResult.Errors.Concat(passwordResult.Errors).ToList()
        };
    }
}
// Responsibility: Data persistence
public interface IUserRepository
{
    Task<User> GetByIdAsync(int id);
    Task<User> GetByEmailAsync(string email);
    Task AddAsync(User user);
    Task UpdateAsync(User user);
    Task DeleteAsync(int id);
}

public class UserRepository : IUserRepository
{
    private readonly DbContext _context;

    public UserRepository(DbContext context)
    {
        _context = context;
    }

    public async Task<User> GetByIdAsync(int id)
        => await _context.Users.FindAsync(id);

    public async Task<User> GetByEmailAsync(string email)
        => await _context.Users.FirstOrDefaultAsync(u => u.Email == email);

    public async Task AddAsync(User user)
    {
        await _context.Users.AddAsync(user);
        await _context.SaveChangesAsync();
    }

    public async Task UpdateAsync(User user)
    {
        _context.Users.Update(user);
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var user = await GetByIdAsync(id);
        if (user != null)
        {
            _context.Users.Remove(user);
            await _context.SaveChangesAsync();
        }
    }
}
// Responsibility: Email notifications
public interface IEmailService
{
    Task SendWelcomeEmailAsync(string email, string name);
    Task SendPasswordResetEmailAsync(string email, string resetToken);
}

public class EmailService : IEmailService
{
    private readonly IEmailSender _sender;
    private readonly IEmailTemplateEngine _templates;

    public EmailService(IEmailSender sender, IEmailTemplateEngine templates)
    {
        _sender = sender;
        _templates = templates;
    }

    public async Task SendWelcomeEmailAsync(string email, string name)
    {
        var body = await _templates.RenderAsync("Welcome", new { Name = name });
        await _sender.SendAsync(email, "Welcome to Our Platform!", body);
    }

    public async Task SendPasswordResetEmailAsync(string email, string resetToken)
    {
        var body = await _templates.RenderAsync("PasswordReset", new { Token = resetToken });
        await _sender.SendAsync(email, "Reset Your Password", body);
    }
}
// Responsibility: User registration orchestration
public class UserRegistrationService
{
    private readonly IUserValidator _validator;
    private readonly IUserRepository _repository;
    private readonly IPasswordHasher _passwordHasher;
    private readonly IEmailService _emailService;
    private readonly ILogger<UserRegistrationService> _logger;

    public UserRegistrationService(
        IUserValidator validator,
        IUserRepository repository,
        IPasswordHasher passwordHasher,
        IEmailService emailService,
        ILogger<UserRegistrationService> logger)
    {
        _validator = validator;
        _repository = repository;
        _passwordHasher = passwordHasher;
        _emailService = emailService;
        _logger = logger;
    }

    public async Task<RegistrationResult> RegisterAsync(string email, string password, string name)
    {
        // Validate
        var validation = _validator.ValidateRegistration(email, password);
        if (!validation.IsValid)
            return RegistrationResult.Failed(validation.Errors);

        // Check if user exists
        var existingUser = await _repository.GetByEmailAsync(email);
        if (existingUser != null)
            return RegistrationResult.Failed("Email already registered");

        // Create user
        var user = new User
        {
            Email = email,
            Name = name,
            PasswordHash = _passwordHasher.Hash(password),
            CreatedAt = DateTime.UtcNow
        };

        await _repository.AddAsync(user);

        // Send welcome email (fire and forget, or use background job)
        _ = _emailService.SendWelcomeEmailAsync(email, name);

        _logger.LogInformation("User registered: {Email}", email);

        return RegistrationResult.Success(user.Id);
    }
}

Benefits of SRP

Benefit Description
Easier Testing Test each class in isolation
Better Reuse EmailService can be used anywhere, not just for users
Easier Changes Changing email provider only affects EmailService
Clearer Code Each class has a clear, focused purpose
Team Collaboration Different developers can work on different components

How to Identify SRP Violations

Ask these questions about your class:

  1. Does it have multiple “actors” that could request changes?

    • Business: validation rules
    • IT: database structure
    • Marketing: email templates
  2. Does it mix different levels of abstraction?

    • High-level: business logic
    • Low-level: database queries, HTTP calls
  3. Is the class hard to name?

    • Names like UserManager, OrderProcessor, DataHandler often indicate multiple responsibilities
  4. Does it have many private methods?

    • Often indicates hidden responsibilities that should be extracted

Interview Tips

Common Questions:

  • “Explain SRP with an example”
  • “How do you decide when to split a class?”
  • “Can SRP lead to too many classes?”

Key Points:

  1. SRP is about reasons to change, not just “doing one thing”
  2. Related to cohesion - methods in a class should be related
  3. Yes, it can lead to more classes, but each is simpler
  4. Balance: don’t create a class for every method

Code Smell Indicators:

  • Class names ending in “Manager”, “Processor”, “Handler”
  • Classes with many dependencies (> 5-7)
  • Classes with unrelated methods
  • Long classes (> 200-300 lines is a warning sign)

📚 Related Articles