← All writing
Craft · · 6 min

Liquid is not a programming language

On a language with no escape hatch, the request you cannot make, and what you build instead once you stop trying to win the argument.

Shopify Ecommerce

Second Shopify build this year, and I spent most of a week irritated.

The thing I wanted was ordinary: on a product page, look up something related that isn’t in the standard relationship, and render it. On the platform I’d spent four years in I’d have written a query, or called a model, or in a genuinely bad mood added a method to a block class and used it in the template.

Here there’s no query. There’s no model. There is a template, and the template has access to exactly the objects the platform decided to hand it, and there is no sentence you can write in that file that goes and gets anything else.

I read this as a limitation for about five days. I now think it’s the most interesting design decision in the product.

What it can’t do, precisely

Liquid has loops, conditionals, filters and assignment. It looks enough like a programming language to set the wrong expectation.

What it hasn’t got is the ability to initiate anything. No I/O. No network calls, no database access, no filesystem, no importing a library, no defining a function you can reuse, no recursion. You cannot make the template ask a question that the platform didn’t already anticipate and answer before rendering began.

Coming from a platform where the template could reach the whole application, that feels like being handed scissors with the blades glued.

A template that cannot ask for anything can also never be the reason the site is slow, and cannot be the hole somebody's theme lets an attacker through.

Why it’s built that way

The reason is that these templates run on somebody else’s servers, for 100,000 stores, and the person editing them is frequently not a developer.

If a theme could run arbitrary code, then one badly written theme could take down a shared machine, and one malicious theme could read another store’s data, and every merchant on the platform would be one bad app install away from a very bad day. The whole model, where you rent a very good machine and somebody else keeps it running, requires that tenants cannot do arbitrary things.

Which means the constraint I was annoyed by is actually incredibly important for the thing I actually value about the platform, which is not being awake at 3 am. I had been treating those as separate facts about the product. They’re the same fact from two sides.

There’s a performance consequence too, and it’s the one I’d underweighted. The template physically cannot generate an N+1, because it cannot generate a 1. Every byte of data the page needs was assembled before rendering started. I have spent weeks of my life on pages that got slow because a template did something in a loop, and that entire category of problem does not exist here.

What you do instead

Once I stopped trying to win, the patterns are clear enough and there are basically four.

Put it in the data. Metafields on the object, populated ahead of time by whatever process actually knows the answer. This is the right answer far more often than it feels like, because most of what I wanted to compute at render time was stable for hours or days and had no business being computed per-request.

Precompute into a section or a snippet. If it’s a fixed relationship, denormalize it where the merchant can edit it. Slightly redundant, entirely legible, and a person who isn’t me can change it without a deploy.

Move it to the client. The one genuine escape hatch is that the rendered page can fetch. If the thing is dynamic and per-visitor, that’s where it lives, the same hole-punching pattern as any cached page, and for the same reason.

Move it off the platform. A small service does the work and writes the result back through the API. This is the honest answer when the requirement is genuinely computational, and the tell that you’re here is that you’ve been arguing with the template for more than a day.

The general shape

I’ve been turning over why this took a week to accept, and I think it’s that constraint reads as an insult to competence. I could write the query. Being told I can’t feels like being told I might get it wrong.

But the constraint isn’t about me. It’s a property of the system that makes a promise possible, uptime, isolation, a floor under how badly any one tenant can behave, and those promises are worth more to the businesses I build for than my ability to do something clever at render time is worth to anybody.

That’s the same trade as a support matrix, or a performance budget, or any of the other things I’ve written about that are really just somebody deciding in advance what won’t be allowed. Constraints applied at the platform level are the only ones that hold, because they don’t rely on every individual remembering to be good.

Grateful, eventually

I’ve come round so far that I now miss it going the other way. Back on a self-hosted store last month I caught a template doing a lookup inside a loop, on a page that gets 40,000 views a day, and my honest first thought was that this would simply not have been expressible on the other platform.

Which is a strange thing to find yourself grateful for, having spent a week in June being annoyed about precisely that 🙃

Read similar posts
7 min

The checkout isn't yours

Shopify and Magento aren't really competing on features, they're competing on how much of your business logic you're allowed to have, and the answer depends less on your catalog than on whether you have engineers.

7 min

Stop and ask me

Three Claude skills cover the whole life of a Shopify client repo at work, and after writing all three I'm fairly sure the steps are the least valuable thing in them. The value is the four or five places where the runbook is told to refuse.