SRP in practice — refactoring a god class
Concept
Refactoring a god class is one of the most concrete engineering exercises you can do to internalize SRP. A "god class" (sometimes called a "blob" or "kitchen-sink class") is a class that knows and does too much. It accumulates responsibilities over time because it is always the "convenient" place to add new logic. The refactoring process is systematic: identify responsibilities, extract them one at a time, wire the pieces together with dependency injection.
The safest approach is strangler fig refactoring: you do not rewrite the class from scratch. Instead, you carve out one responsibility, create a new focused class for it, update the god class to delegate to the new class, run your tests, and repeat. Each step leaves the system in a working state.
The process in four steps:
- Map responsibilities — list every logical concern inside the class. Look for: different nouns it operates on, different stakeholders it serves, different subsystems it reaches (DB, email, payment, cache).
- Identify seams — find the natural boundaries where one responsibility ends and another begins. Method groups, property clusters, and import clusters are seams.
- Extract and test — move one responsibility at a time into its own class, inject it back as a dependency, test both the new class and the updated original.
- Rename and document — once extraction is complete, rename the original class to accurately reflect its reduced role.
Code Example
<?php
declare(strict_types=1);
// BEFORE: God class — User model doing everything
class User extends Model
{
// Identity data — OK, belongs here
protected $fillable = ['name', 'email', 'password'];
// Relationship — OK
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
// WRONG: billing concern on a user model
public function hasActiveSubscription(): bool
{
return $this->subscriptions()
->where('status', 'active')
->where('expires_at', '>', now())
->exists();
}
// WRONG: notification concern
public function sendWelcomeEmail(): void
{
Mail::to($this->email)->send(new WelcomeEmail($this));
}
// WRONG: authentication concern (beyond what the model needs to know)
public function generateRememberToken(): string
{
$token = bin2hex(random_bytes(32));
$this->update(['remember_token' => hash('sha256', $token)]);
return $token;
}
// WRONG: permission concern
public function can(string $permission): bool
{
return $this->roles()
->whereHas('permissions', fn($q) => $q->where('name', $permission))
->exists();
}
}
// AFTER: Each responsibility extracted to its own class
// Model stays lean — identity, relationships, casts
class User extends Model
{
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
public function subscriptions(): HasMany
{
return $this->hasMany(Subscription::class);
}
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class);
}
}
// Billing concern lives here
final class SubscriptionChecker
{
public function hasActiveSubscription(User $user): bool
{
return $user->subscriptions()
->where('status', 'active')
->where('expires_at', '>', now())
->exists();
}
}
// Notification concern lives here
final class UserNotifier
{
public function sendWelcomeEmail(User $user): void
{
Mail::to($user->email)->send(new WelcomeEmail($user));
}
}
// Authorization concern lives here (or in a Laravel Policy)
final class PermissionChecker
{
public function can(User $user, string $permission): bool
{
return $user->roles()
->whereHas('permissions', fn($q) => $q->where('name', $permission))
->exists();
}
}
// Registration action wires it together
final class RegisterUserAction
{
public function __construct(
private readonly UserNotifier $notifier,
) {}
public function execute(array $data): User
{
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
$this->notifier->sendWelcomeEmail($user);
return $user;
}
}Interview Q&A
Q: How do you refactor a god class safely without breaking production?
The golden rule is: never delete before you extract. Start by writing characterization tests — tests that document the current behavior of the god class without judging whether it is good or bad. These become your safety net. Then extract one responsibility at a time using the strangler fig pattern: create the new class, make the god class delegate to it (inject the new class as a constructor dependency), run all tests, commit. Repeat until the god class is just an orchestrator of its extracted dependencies, then rename it appropriately. Never do a big-bang rewrite — it leaves you with nothing running until the rewrite is complete, and large rewrites almost always miss edge cases.
Q: What is the relationship between SRP and testability?
They are directly linked. A god class is hard to test because you need to mock out every external dependency it holds (database, email, payment gateway) just to test a single logical path. A class with a single responsibility typically has one or two dependencies, making unit tests trivial to write. When you find yourself writing test setup code longer than the test body, it is usually a sign that the class under test is doing too much. SRP violations are one of the biggest drivers of test fragility — a change to the payment system should not break tests for the notification system, but it will if both live in the same class.
Q: When is it OK to leave a class with multiple responsibilities?
There are two legitimate exceptions. First, orchestrators and use-case objects — action classes, command handlers, and application services are intentionally the "glue" that coordinates multiple focused objects. Their responsibility is the coordination, and they are allowed to call many other classes. Second, small, throwaway scripts — a one-off data migration script that will run once and be deleted does not need to be split into five classes. Apply SRP rigorously where code lives long and changes often. The principle pays dividends in proportion to how frequently a codebase is modified.