S — Single Responsibility Principle: one reason to change
Concept
The Single Responsibility Principle states that a class should have one, and only one, reason to change. Robert C. Martin coined the phrase, but the underlying idea is older: a unit of code should be responsible to exactly one actor — one stakeholder, one part of the system, one axis of change.
The word "responsibility" is where most developers get it wrong. It does not mean a class can only have one method. A User model can legitimately have dozens of methods — as long as every method serves the same stakeholder. What SRP actually means is: if you can imagine two different people (a DBA, a marketing manager, a billing engineer) asking you to change this class for entirely unrelated reasons, it has too many responsibilities.
The practical test is the "reason to change" question. Ask: who would ask me to modify this class? If the answer is "both the payments team and the reporting team", the class is doing too much. The moment it needs to change for a payments reason and a reporting reason independently, those two concerns belong in separate classes.
In PHP codebases, the most common SRP violations appear in three places: fat controllers that validate, save, send email, and generate PDFs all in one action method; fat models (Eloquent User classes that handle authentication, authorization, billing, and notifications); and service classes that grow into "do everything" helpers.
| Violates SRP | Respects SRP |
|---|---|
UserController::store() validates, hashes password, saves, sends welcome email, logs to audit | UserController::store() calls CreateUserAction, which delegates email to SendWelcomeEmail |
User model has sendPasswordReset(), generateInvoice(), getPermissions() | User model only manages identity data; PasswordResetService, InvoiceService, AuthorizationService own the rest |
OrderService fetches from DB, applies discounts, sends to payment gateway, updates inventory | Separate DiscountCalculator, PaymentGateway, InventoryManager classes |
Code Example
<?php
declare(strict_types=1);
// VIOLATION: One class handles order logic AND notification AND persistence
class OrderProcessor
{
public function process(array $orderData): void
{
// Concern 1: Business logic
$total = 0;
foreach ($orderData['items'] as $item) {
$total += $item['price'] * $item['quantity'];
}
// Concern 2: Persistence
$pdo = new \PDO('mysql:host=localhost;dbname=shop', 'root', '');
$stmt = $pdo->prepare('INSERT INTO orders (total) VALUES (?)');
$stmt->execute([$total]);
// Concern 3: Notification
mail($orderData['email'], 'Order confirmed', "Your total is {$total}");
}
}
// CORRECT: Each class has exactly one reason to change
final class OrderCalculator
{
public function calculateTotal(array $items): float
{
return array_sum(
array_map(
fn(array $item): float => $item['price'] * $item['quantity'],
$items
)
);
}
}
final class OrderRepository
{
public function __construct(private readonly \PDO $pdo) {}
public function save(float $total, int $customerId): int
{
$stmt = $this->pdo->prepare(
'INSERT INTO orders (customer_id, total) VALUES (?, ?)'
);
$stmt->execute([$customerId, $total]);
return (int) $this->pdo->lastInsertId();
}
}
final class OrderNotifier
{
public function sendConfirmation(string $email, float $total): void
{
// Could be Mailgun, SES, etc. — only the notifier knows
mail($email, 'Order confirmed', "Your total is {$total}");
}
}
// The orchestrating action — thin, it just wires the pieces together
final class PlaceOrderAction
{
public function __construct(
private readonly OrderCalculator $calculator,
private readonly OrderRepository $repository,
private readonly OrderNotifier $notifier,
) {}
public function execute(array $orderData): int
{
$total = $this->calculator->calculateTotal($orderData['items']);
$orderId = $this->repository->save($total, $orderData['customer_id']);
$this->notifier->sendConfirmation($orderData['email'], $total);
return $orderId;
}
}Interview Q&A
Q: What does "one reason to change" actually mean in practice?
It means one stakeholder or one concern drives changes to the class. If your User model changes when marketing wants a new profile field AND when the security team wants to change how passwords are hashed AND when billing wants new payment metadata, those are three different reasons — three different stakeholders. Split the class at those seams. A practical heuristic: can you write a git commit message for a change to this class that clearly names a single department or feature area? If not, the class spans too many concerns.
Q: How does SRP relate to the Laravel fat model / fat controller anti-pattern?
Fat models accumulate responsibilities because Eloquent makes it trivially easy to add methods to a model. Over time User ends up with sendPasswordReset(), getBillingAddress(), hasActiveSubscription(), getPermissions(), and generateApiToken(). Each of these serves a completely different stakeholder. The fix is to move domain logic into service classes, action classes, or dedicated domain objects while keeping the Eloquent model responsible only for the data mapping layer. Same with fat controllers: validation logic belongs in a FormRequest, business logic belongs in a service or action, and the controller's job is just to accept HTTP input and return an HTTP response.
Q: Is it wrong to have a class with 20 methods if they all serve the same concern?
No. SRP is not about the number of methods; it is about the coherence of responsibility. A Money value object could have add(), subtract(), multiply(), format(), toCents(), toFloat(), getCurrency(), equals(), and many more methods — all of which serve the single concern of representing and operating on monetary values. None of those methods would be changed by a different stakeholder than the others. That class respects SRP despite having many methods. The signal to watch for is not method count but whether you can give the class a single, crisp name that makes all of its methods obviously belong together.