SOLID in Practice — S: Single Responsibility Principle

Understand what the Single Responsibility Principle (SRP) really means, why most developers misinterpret it, and how to apply it with real C# examples. SRP is not about doing one thing. It's about having a single reason to change. Understand the difference and see how it reshapes the way you structure your code.

April 27, 20264 min readTawan Silva
SOLID in Practice — S: Single Responsibility Principle

Share this post

Send it to someone or save the link for later.

SOLID in Practice — S: Single Responsibility Principle

SRP is the most cited and most misunderstood principle in SOLID.

The definition everyone learns is: "a class should do only one thing". But that's too vague to be useful. A class that saves a user to the database "does one thing". A class that validates, saves, and sends an email can also be described as "doing one thing" — managing users.

The correct definition, from Robert Martin, is different:

A class should have only one reason to change.

That changes everything. It's not about the number of methods or lines of code — it's about how many distinct sources of change can force you to modify that class.


The problem: a class with multiple reasons to change

// this class has at least 3 reasons to change:
// 1. user business rules change
// 2. email format changes
// 3. database schema changes

public class Usuario
{
    public string Nome { get; set; }
    public string Email { get; set; }

    public Usuario(string nome, string email)
    {
        Nome = nome;
        Email = email;
    }

    // reason 1: persistence logic
    public void Salvar(SqlConnection connection)
    {
        var cmd = new SqlCommand(
            "INSERT INTO Usuarios (Nome, Email) VALUES (@Nome, @Email)",
            connection
        );
        cmd.Parameters.AddWithValue("@Nome", Nome);
        cmd.Parameters.AddWithValue("@Email", Email);
        cmd.ExecuteNonQuery();
    }

    // reason 2: email logic
    public void EnviarBoasVindas()
    {
        var client = new SmtpClient("smtp.gmail.com", 587);
        var mensagem = new MailMessage(
            "[email protected]",
            Email,
            "Welcome!",
            $"Hi, {Nome}! Welcome aboard."
        );
        client.Send(mensagem);
    }

    // reason 3: presentation logic
    public string GerarRelatorio()
    {
        return $"User: {Nome} | Email: {Email}";
    }
}

Why is this a problem?

If the infrastructure team migrates the database to PostgreSQL, you open Usuario to change Salvar. If the marketing team changes the welcome email template, you open the same class to change EnviarBoasVindas. These are completely independent changes — but they all live in the same place.

This violates SRP because the class has multiple actors (teams/responsibilities) that can force it to change.


The solution: separate by responsibility

// each class has a single reason to change

// responsibility 1: represent and validate user data
public class Usuario
{
    public string Nome { get; }
    public string Email { get; }

    public Usuario(string nome, string email)
    {
        if (string.IsNullOrWhiteSpace(nome) || nome.Length < 2)
            throw new ArgumentException("Invalid name");

        if (!email.Contains("@"))
            throw new ArgumentException("Invalid email");

        Nome = nome;
        Email = email;
    }
}


// responsibility 2: persistence — changes only if the database changes
public class UsuarioRepository
{
    private readonly SqlConnection _connection;

    public UsuarioRepository(SqlConnection connection)
    {
        _connection = connection;
    }

    public int Salvar(Usuario usuario)
    {
        var cmd = new SqlCommand(
            "INSERT INTO Usuarios (Nome, Email) VALUES (@Nome, @Email); SELECT SCOPE_IDENTITY();",
            _connection
        );
        cmd.Parameters.AddWithValue("@Nome", usuario.Nome);
        cmd.Parameters.AddWithValue("@Email", usuario.Email);
        return Convert.ToInt32(cmd.ExecuteScalar());
    }

    public Usuario? BuscarPorEmail(string email)
    {
        var cmd = new SqlCommand(
            "SELECT Nome, Email FROM Usuarios WHERE Email = @Email",
            _connection
        );
        cmd.Parameters.AddWithValue("@Email", email);

        using var reader = cmd.ExecuteReader();
        if (!reader.Read()) return null;

        return new Usuario(reader.GetString(0), reader.GetString(1));
    }
}


// responsibility 3: notification — changes only if email logic changes
public class ServicoNotificacao
{
    private readonly string _smtpHost;
    private readonly int _smtpPort;

    public ServicoNotificacao(string smtpHost, int smtpPort)
    {
        _smtpHost = smtpHost;
        _smtpPort = smtpPort;
    }

    public void EnviarBoasVindas(Usuario usuario)
    {
        var client = new SmtpClient(_smtpHost, _smtpPort);
        var mensagem = new MailMessage(
            "[email protected]",
            usuario.Email,
            "Welcome!",
            $"Hi, {usuario.Nome}! Welcome aboard."
        );
        client.Send(mensagem);
    }
}


// orchestrates everything: the use case / application service
public class CadastrarUsuarioUseCase
{
    private readonly UsuarioRepository _repository;
    private readonly ServicoNotificacao _notificacao;

    public CadastrarUsuarioUseCase(
        UsuarioRepository repository,
        ServicoNotificacao notificacao)
    {
        _repository = repository;
        _notificacao = notificacao;
    }

    public Usuario Executar(string nome, string email)
    {
        var usuario = new Usuario(nome, email);
        _repository.Salvar(usuario);
        _notificacao.EnviarBoasVindas(usuario);
        return usuario;
    }
}

Now each class has a single owner. If the database changes, only UsuarioRepository is modified. If the email changes, only ServicoNotificacao. The Usuario class only changes if the user's business rules change.


How to spot SRP violations in your code

"Who would ask for this to change?" If the answer is more than one team or role (database team, email team, business team), you're probably violating SRP.

"When I test this class, how many things need to be mocked?" If a unit test for Usuario needs to mock the database, SMTP, and a logging service at the same time, the class is doing too much.

"If I change the database, how many files unrelated to the database do I need to touch?" The answer should be: only the repository/persistence files.


Summary

Before

After

One class handles validation, persistence, and email

Each responsibility in its own class

Database change affects email code

Database change only affects the repository

Tests need to mock everything

Isolated, simple unit tests

Hard to reuse individual parts

ServicoNotificacao can be used in other flows

SRP is the foundation of everything that comes next in SOLID. With it, code starts to organize itself naturally.


Next in the series: O — Open/Closed Principle. How to write code that accepts new functionality without needing to be modified.

Support the content

Enjoyed the post? Buy me a coffee.

If this helped you, you can support the content with a small PayPal contribution.

Related posts

Keep reading from topics connected to this post.

Comments

Comments are moderated before they become visible.

Sign in to comment on this post.

Kamilla

05/02/2026

Muito bom

Tawan

04/30/2026

Teste