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 |
|
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.

