Back to blog

One Feature, Five Patterns: How I Get My Head Around a Laravel Codebase

Laravel PHP Design,Patterns

Design patterns are easy to explain in isolation. They’re much harder to recognise when you’ve just opened a four-year-old Laravel app and someone is asking for a “small tweak” to how subscription upgrades work.

When I land in a codebase, I’m not trying to spot patterns for fun. I’m trying to understand data flow. Where does the request enter? What actually changes state? What decisions are made? What happens afterwards?

Let’s take a concrete example: an organisation upgrades their subscription plan. It sounds simple. It rarely is.

If you trace that feature properly, you’ll usually find at least five patterns layered together. The trick is learning to recognise the shapes.

  1. The Verb (Command / Action)

Every feature has a verb. Upgrade. Cancel. Approve.

Somewhere in the codebase there is a thing that represents “this happens”. In a tidy Laravel app, that’s often an action class:

class UpgradeOrganisationPlan
{
    public function __invoke(
        Organisation $organisation,
        Plan $newPlan,
        User $actor
    ): Subscription {
        // upgrade logic
    }
}

This is the spine of the feature. When I’m trying to understand something, I search for the verb first. Not the controller. Not the blade view. The verb.

If the controller is doing everything itself, the feature still exists, it’s just smeared across layers. My job then is to reconstruct that spine mentally before I touch anything.

Find the verb. You’ve found the centre of gravity.

  1. The Choice (Strategy)

Upgrading a plan isn’t always a single path. You might have Stripe for some tenants and manual billing for others. You might prorate. You might schedule the change at renewal.

When the goal is the same but the implementation varies, you’re looking at strategy.

In Laravel, that often looks like this:

interface BillingGateway
{
    public function upgrade(Organisation $org, Plan $plan): void;
}

With different implementations behind it.

What I’m really asking here is: where does behaviour vary? If I see large if ($provider === 'stripe') blocks scattered around, that tells me the system has multiple algorithms but no clear boundary between them.

Strategy is simply “same outcome, different way”.

  1. The Gate (Policy / Specification)

Before anything upgrades, something decides whether it’s allowed.

Laravel Policies usually handle the “who”:

public function upgrade(User $user, Organisation $org): bool
{
    return $user->isOwnerOf($org);
}

But real systems often have deeper rules: billing status, seat limits, plan eligibility, trial constraints.

When I’m mapping a feature, I deliberately look for where the system says “no”. That’s the gate. If those rules are scattered across controllers, models and listeners, changes become risky very quickly.

Separating the gate from the verb makes everything easier to reason about.

  1. The Ripple (Events)

Upgrading a plan almost never ends at the database update. Entitlements change. Emails go out. Audit logs are written. External systems are notified.

In Laravel, that often looks like:

event(new PlanUpgraded($organisation, $newPlan));

With listeners handling the side effects.

Events are great for decoupling, but they introduce hidden data flow. When I’m planning a change, I always trace what fires afterwards. A small tweak in the action can have half a dozen consequences downstream.

Follow the ripple.

  1. Time (Jobs)

Anything involving billing providers, emails or third-party APIs probably shouldn’t block the request.

That’s where jobs come in:

class SyncEntitlements implements ShouldQueue
{
    public function handle(): void
    {
        // external API calls
    }
}

Jobs shift work into another timeline. Now you have retries, potential duplication, and eventual consistency. The moment you see ShouldQueue, you need to think about timing, not just logic.

Time changes the shape of the system.

When I enter a new Laravel project, I don’t try to name patterns academically. I build a map.

Find the entry point. Identify the verb. Locate where behaviour varies. Find the gate. Trace the ripple. Notice where time shifts.

Patterns are just labels for these shapes. The real goal is predictability. In mature Laravel applications, the ability to predict consequences before you type is far more valuable than knowing the textbook definition of Strategy.

You Might Also Like