Shopify Theme Architectural Decisions That Matter in Year Two
Choices you make in week one that come back to bite in year two. Section naming, block limits, metaobject vs metafield, global sections strategy. What we do differently the second time around.
A Shopify theme is easy to launch and painful to maintain. In week one the codebase is small, the section list fits on one screen, the merchant has not yet saved a single template, and the only content model is the default product. By month fourteen the same theme has forty sections, a theme.liquid that looks like a landfill of tracking snippets, a settings panel with two hundred toggles, and a product metafield that should have been a metaobject but now lives on three hundred products. The distance between those two states is almost entirely a function of decisions made before the theme ever went live.
We have rebuilt or maintained more than two hundred and fifty Shopify themes at WitsCode, and the same small set of architectural choices separates the themes that are pleasant in year two from the ones that have to be thrown out and redone. This article is about those choices. It is not a restatement of the Shopify dev docs. It is what we do differently the second time around, after we have lived with our first decisions long enough to regret them.
The Block Limit Is Not the Ceiling You Think It Is
Shopify allows up to seventeen blocks per section. That number is a hard technical ceiling, and because it is published clearly in the documentation a lot of themes treat it as a target. They end up with a single mega section that accepts image blocks, text blocks, icon blocks, video blocks, product blocks, quote blocks, spacer blocks, and a handful of conditional blocks that only render when a particular toggle is flipped. On the dev machine it looks elegant. One section, maximum flexibility, minimal file count. In the merchant interface it is a disaster.
The real ceiling on block count is not technical, it is cognitive. A merchant who needs to edit the testimonial that sits on the homepage has to open the right section, scroll past seven unrelated block types, identify the one that is a testimonial, click into it, and try to remember which of the four settings on that block controls the quote text. If the block list is more than five types long, the merchant starts making mistakes. They duplicate blocks they did not mean to. They edit the wrong instance. They ask support to do it for them.
The correct response is not to add an eighteenth block type, it is to split the section. A hero is a section. A featured collection grid is a section. A testimonial marquee is a section. Each one has a narrow set of block types that make sense only inside that container. When a page needs to combine five of these, you use a section group or a JSON template, not a single section with seventeen block slots. Section groups exist precisely so that you do not have to flatten the page into one container. The block limit is an escape valve for genuinely composite sections like a rich-text-and-image module, not a design goal.
Naming Is an API Contract
Once a merchant saves a template, the section type and block type strings become part of a contract that is expensive to break. JSON templates reference sections by type. Block instances in those templates reference blocks by type. If you rename collection1.liquid to featured-collection-grid.liquid in year two, every template that references the old type stops rendering the section, and the merchant sees an empty slot with no obvious cause.
This is why the names you pick on day one matter more than the names of almost anything else in the codebase. A section named section-1 or collection1 or custom-block is a promise to rename it later, and that rename will either never happen or will require a migration script that rewrites every merchant-saved template. We use a strict convention. Sections are named noun-modifier in kebab-case. featured-collection-grid, testimonial-marquee, hero-video, icon-row-three-up. Blocks are named by the noun they represent. testimonial, icon, feature, product-card. The schema name field matches the filename so that the theme editor labels match the code.
The naming rule that catches the most mistakes is the one about presets. Every preset shown in the Add section picker is a user-facing label, and it should describe what the merchant gets, not what the engineer called it. "Featured collection - four up" and "Featured collection - carousel" are real labels. "Default" is a label that tells the merchant nothing and guarantees that half of them will pick it by accident and then wonder why their homepage does not look like the demo.
The Metaobject vs Metafield Decision Has a Single Test
This is the year-two decision that causes the most expensive rework. A merchant asks you to add a size guide to every product, and it is tempting to reach for a product metafield because metafields are familiar and the plumbing is short. So you add a rich text metafield called custom.size_guide and you render it on the product page. Six months later the merchant has three hundred products, two thirds of them share the same size guide, and when the guide changes they have to edit three hundred metafields.
The test we use is simple. If the content would be copy-pasted across more than two resources, it is a metaobject. If it is genuinely unique to the resource, it is a metafield. Product care instructions that vary per product, a product-specific warranty PDF, a per-variant spec sheet, those are metafields. Size guides, author bios, store locations, ingredient glossaries, designer profiles, testimonial library entries, those are metaobjects. The idiomatic pattern that ties the two together is a metaobject reference metafield on the product. The product has a metafield called custom.size_guide whose type is a reference to a size_guide metaobject. The merchant picks the right size guide from a dropdown in the admin. Updating the guide updates every product that references it.
The cost of getting this wrong is not theoretical. Migrating from a product-level metafield to a metaobject in year two requires an Admin API script that reads every product, deduplicates the content into metaobject entries, replaces the field reference, and updates every template that rendered the old field. We have done that migration often enough that we now apply the decision rule by default on every new theme, even when the merchant insists the content will always be unique. It rarely stays unique.
Global Sections Belong in Section Groups, Not theme.liquid
The second most common year-two mess we walk into is theme.liquid. On a fresh theme it is fifty lines of head, a body tag, a header section, {{ content_for_layout }}, a footer section, and a closing body. By the time we see it in year two it has forty lines of conditional app snippets in the head, three different analytics pixels inline, a consent banner hardcoded before the header, a promotional bar that was supposed to be temporary, and a {% section 'custom-announcement' %} tag that a previous developer added because they did not know about section groups.
The rule we follow is that theme.liquid contains almost nothing. Head content is split into small snippets like head-fonts.liquid, head-meta.liquid, head-scripts.liquid, each of which is responsible for one concern and can be removed without touching the others. Tracking pixels go through content_for_header and app blocks so that they can be turned on and off from the Shopify admin, not the codebase. Anything that looks like a region, a header, a footer, an aside, a promotional bar, a consent banner, goes into a section group JSON file. Section groups let merchants reorder, add, and remove sections from global regions without a developer, and they make theme.liquid boring, which is what you want.
A boring theme.liquid is readable. You can diff it year over year and see what changed. A theme.liquid that is a dumping ground is the file that no one wants to touch because no one is sure which of the fourteen app snippets inside it are still live.
Preset Sprawl Is a Merchant Problem, Not a Dev Problem
Every preset you add to a section is a choice the merchant has to make. If your featured collection section has eight presets, the merchant sees eight options in the Add section picker and has to pick one. Most of them will pick the first one, which means seven of your presets exist to serve a minority of users who know exactly what they want. That minority is usually an engineer on your team, not the merchant.
Our rule is at most three presets per section. One opinionated default that reflects how the section is most often used. One alternative that covers the second most common case. One edge case if there is a genuine third mode. If you find yourself wanting a fourth preset, that is almost always a signal that you need a second section rather than a fourth preset. A carousel is not a preset of a grid, it is a different section with different block types and different layout logic.
Preset content matters too. The default blocks that ship inside a preset should be plausible filler, not lorem ipsum, and the count should match the preset name. "Featured collection - four up" should ship with four product card blocks preconfigured. "Testimonial marquee" should ship with three testimonials. If the merchant has to add every block manually the preset is doing none of the work it exists to do.
Schema Settings Have a Half-Life
The global settings in settings_schema.json are the easiest architectural decision to get wrong because each individual addition feels harmless. A new brand color, a new font size, a new header layout toggle. Each one is a five-line change. By the end of year one the settings panel has grown to one hundred and fifty options organized into twenty tabs, and no one on the merchant side can find the setting they need.
The discipline we apply is that a global setting has to meet two tests. It has to be genuinely global, meaning it affects more than one section, and it has to be a setting the merchant will plausibly change. Brand colors, typography, base spacing, logo, favicon, default transition speed, those pass. A toggle that controls whether the header is sticky on mobile when the announcement bar is hidden and the cart drawer is open, that does not pass. That is a section setting on the header section, and it lives with the section that uses it.
Section-level settings can grow more freely because they are scoped to the merchant who is editing that specific section. Global settings are a shared resource and they compound. Treat them like you would treat entries in a constants file in any other codebase.
Plan the Rename Before You Need It
Not every bad decision is avoidable on day one. Sometimes a section turns out to serve a different purpose than you planned, sometimes a naming convention evolves, sometimes Shopify ships a new primitive that makes an old structure obsolete. The architectural choice that matters here is whether you planned for renames at all.
A theme that planned for renames has a JSON template migration workflow ready. It has a deploy process that runs a script against the live store, reads the templates, rewrites section types and block types, and pushes them back. It keeps a mapping of old type names to new type names so that the rewrite is idempotent. A theme that did not plan for renames treats every rename as a crisis, which means most renames never happen, which means the bad names accumulate until the whole theme has to be replaced.
Even a basic migration discipline pays for itself the first time you use it. We keep a migrations/ directory in every theme repo, each file dated and describing what section or block was renamed and what the old type was. The deploy process reads that directory, applies any pending migrations to the live store, and marks them as applied. It is not complicated, and it turns "we cannot rename this section" into "we can rename anything as long as we write the migration".
What We Do Differently the Second Time Around
The themes we build now look different from the themes we built three years ago in small, specific ways. Sections are narrower and more numerous. Presets are fewer and better named. Metaobjects show up on day one instead of year two. theme.liquid is short. Global settings are rationed. Every block type earns its place by doing something a section setting cannot do.
None of this is visible to the merchant on launch day. The theme looks the same. The pages render the same. The speed is the same. The difference only shows up in month fourteen, when the merchant asks for a change that would have required rebuilding half the theme on our old architecture and instead takes an afternoon. Year two is where theme architecture pays back, and the only way to collect that payment is to invest in the architecture before the theme goes live.
If you are looking at a theme that has already hit its year-two wall, or you are about to launch one and you want to avoid the wall entirely, that is what we do. We architect themes for the life they will have, not the life they have on launch day, and we rebuild the ones that were architected for launch day only. ->
Get weekly field notes.
Practical writing on shipping products, straight to your inbox. No spam.
Need help with this?
Shopify Development
We design and build web apps, MVPs, and SaaS products. Talk to us about what you are working on.
Talk to usWant to discuss ecom for your business?
Start a project and we'll talk through where you are, what's working, and the highest-leverage moves for the next 90 days.

