jhosno◆
case ← all work
[PRODUCT · LARAVEL · 2024 — NOW]

[Creatienda]

A system that was already alive, already had customers, and could not stop while I took it apart.

what it is
Multi-tenant e-commerce SaaS
my role
Refactoring & integrations
stack
Laravel · PHP · MySQL
period
2024 — now
outcome
+20% response speed
Wait01 — the system as I found it

Creatienda is a multi-tenant e-commerce SaaS for small businesses across Latin America. Every store is a tenant; every tenant shares the same codebase and the same database cluster, isolated by Spatie Multitenancy.

That architecture is the point and it is also the constraint. There is no window to take the system down, because taking it down takes down every store at once. Nothing here was a rewrite. Every change had to land on a system that kept selling while it changed.

One codebase, many stores. A mistake in the shared core is not one bug — it is every tenant's bug at the same time.

What I walked into

[Describe what already existed: how many tenants, what had been built before you, what the team looked like, what conventions were in place. This is the section that most people skip and it is the one that makes the rest legible.]

Tenants share a core and never touch each other. The horizontal line is the shared code — the part where a mistake multiplies.
Fly02 — the symptom

Two symptoms, and they turned out to have the same shape.

Modules that had grown into each other

Business logic lived in controllers, and controllers knew about each other. Response times sagged under load, but the slowness was the visible part. The real cost was that nobody could change one thing without reading four.

A checkout that owned every provider

Payments in Latin America are not one integration, they are one per country: each provider with its own contract, its own credential shape, its own idea of what a successful response looks like. All of that had been added inside the checkout, one conditional at a time.

So adding a country meant opening the single piece of code where money moves. That is the worst place in any system to be editing under deadline.

The slowness was not the bug. The slowness was the symptom of a system where everything knew about everything.

Everything crossed with everything. This is what a change costs before the refactor: not the edit, the reading.
Caught03 — the diagnosis and what I changed

A service layer, module by module

Business logic moved out of controllers and into services with a single responsibility each. Controllers went back to what they are for: receiving a request, delegating, returning a response. The rule I held to was that no module lands unless the one before it is in production and stable — a big-bang refactor on a live multi-tenant system is not brave, it is reckless.

[Name the specific modules you refactored and the order you did them in. The order is the interesting part: it shows you had a plan, not just an urge.]

A Factory for the gateways

Every provider now sits behind one interface. The checkout asks the factory for a gateway, not for a named provider, and it no longer knows or cares which one it gets.

app/Services/Paymentsillustrative
interface PaymentGateway
{
    public function charge(Order $order): PaymentResult;
    public function refund(Payment $payment): PaymentResult;
}

class PaymentGatewayFactory
{
    /* One place knows the map. Adding a provider means adding
       a class and one line here — the checkout never opens. */
    protected array $drivers = [
        'flow'       => FlowGateway::class,
        'webpay'     => WebpayGateway::class,
        'mercadopago' => MercadoPagoGateway::class,
    ];

    public function for(Tenant $tenant): PaymentGateway
    {
        $key = $tenant->payment_driver;

        if (! isset($this->drivers[$key])) {
            throw new UnsupportedGatewayException($key);
        }

        return app($this->drivers[$key]);
    }
}

The shape matters more than the code: one place knows the map. Adding a provider is a new class plus one line in the factory. The checkout stays closed.

[Add what was genuinely hard: reconciling different callback shapes, idempotency on retries, per-tenant credentials, sandbox vs production. That is what another engineer will recognise as real.]

One node, many outputs. The checkout only ever talks to the node.
Reopen04 — the result

The refactor to a service layer brought a 20% improvement in response speed. That is the number I can put on it, and it is the least interesting part.

What actually changed is the cost of the next change. A new payment provider stopped being an edit to the checkout and became a new file. A new module stopped requiring four files of reading before you could touch one.

The system did not get faster because I made the code pretty. It got faster because it stopped doing the same work in four places.

[If you have any other measurable: error rate, deploy frequency, time-to-add-a-gateway before and after. One more number is worth three more paragraphs.]

+20%

response speed after the service-layer refactor

before
after
The waterfall shortening. Same request, less work behind it.