smplx.
Shopify Architecture

Avoiding Technical Debt in Shopify: Architecture Principles [2026]

Claudio Gerlich··12 min

In our Shopify Architecture Guide, we talk about good architecture. Today: The flip side. Technical debt.

At smplx., we've seen shops that after 3 years couldn't be changed anymore. Not because of the technology. Because of the debt that had accumulated.

The good news: It's avoidable. And if you already have debt — it can be paid down.

What Is Technical Debt?

Definition: Code/architecture that was built quickly but is hard to change.

Metaphor: You take out a quick loan from the bank. Now you pay interest. For a Shopify store, the "interest" is every feature that becomes harder to implement.

How Debt Accumulates

  1. Time Pressure: "We need this feature by Friday!" -> Quick & dirty solution -> Tomorrow a bug -> Patch on top -> After 6 months: Unmaintainable

  2. App Sprawl: "Let's install an app for that" -> First app for discounts -> Second app for bundling -> Third app for variants -> Fifth app later: You have 5 apps, nobody knows how they connect

  3. Undocumented Code: "I know how it works" -> Developer leaves company -> New developer: "Why does this code do that?" -> Can't safely change it -> No new features

  4. Outdated Themes: "Let's keep the old theme" -> Old theme version -> New features not possible -> Missing security updates -> After 2 years: Breakage

  5. Liquid Spaghetti Code

<!-- WRONG: This code actually exists -->
{% if product.handle == "special-product-1" %}
  <div class="special-styling-1">
    {% if customer.email == "vip@customer.com" %}
      <span class="vip-text">VIP Only!</span>
    {% else %}
      <!-- Old code from 2019 -->
      {% for item in cart.items %}
        {% if item.product.handle == "special-product-1" %}
          <!-- Recursive logic - code smell! -->
        {% endif %}
      {% endfor %}
    {% endif %}
  </div>
{% elsif product.handle == "special-product-2" %}
  <!-- 200 Lines of duplicate code -->
{% elsif product.handle == "special-product-3" %}
  <!-- 200 Lines of duplicate code -->
{% endif %}
<!-- This repeats 50 times! -->

Real-World: The Bekateq Story

Bekateq had a situation many are familiar with:

Starting Point (Before):

  • 5 different custom apps
  • 30+ theme snippets with spaghetti logic
  • No tests
  • Hard to understand
  • New features took weeks
  • Bug fixes introduced new bugs

The Cost:

  • Monthly: 2 developers
  • Monthly: 1-2 bugs per week
  • Monthly: Features take 3x longer

The Solution: One App to Rule Them All

Bekateq wrote a single custom app with 4,400 lines of code and zero external dependencies (except the Shopify SDK).

The Result (After):

  • 1 app instead of 5
  • Understandable + documented
  • Testing possible
  • New features: 5 days instead of 15
  • Bugs: Almost zero

The Investment:

  • 8 weeks of refactoring
  • Cost: 50-80k EUR
  • ROI: 6 months (faster development)

Key Learning: The time you save pays for itself.

Signs That You Have Debt

Sign 1: "No, I can't change that"

Developer says: "I can't build that feature,
                 because I can't touch that part of the code."

That's debt. Your code is blocking itself.

Sign 2: Changes Take Longer and Longer

Metric: Velocity drops

Month 1: 10 features
Month 3: 8 features
Month 6: 5 features
Month 12: 2 features

That's debt interest. The older the code, the more "interest."

Sign 3: Tests Are Impossible

"I can't test this code because..."
- "...everything is global"
- "...too many dependencies"
- "...thousands of if-statements"

If code can't be tested, it's debt.

Sign 4: Small Changes, Big Bugs

Changed: Button text
Result: Cart no longer works

That's debt. Tight coupling.

Sign 5: 5+ Apps for Simple Things

- App 1: Discounts
- App 2: Variants
- App 3: Bundling
- App 4: Reviews
- App 5: Inventory
- App 6: Analytics

Each app has:

  • Its own API limits
  • Its own webhooks
  • Its own rate limits
  • Potential conflicts

That's "App Sprawl" debt.

How to Avoid Debt (Architecture-First)

Principle 1: Decide Early — "Build vs. Buy"

Question: Do we need a custom app for this?

Decision Tree:

Feature X needed?
├─ Can it be done with standard Shopify?
│  └─ YES -> Use Standard (no debt!)
│
├─ Need custom code?
│  └─ YES -> Native (Theme) or App?
│     ├─ Theme? (Simple UI logic)
│     │  └─ Yes -> Liquid (fast, no overhead)
│     │
│     └─ App? (Complex Logic, Admin API)
│        └─ One app with clear purpose
│
└─ Can it be done with an existing app?
   └─ YES -> Use App (not custom!)

Example: Discount System

Previously: 3 different apps
   - Discount App A
   - Discount App B
   - Custom Discount App

Better: Shopify's own discount system
   (No custom code needed!)

If custom is needed: 1 app, clear purpose

Principle 2: Write Code for Others

Assumption: Your code will be managed by someone else in 6 months.

Concretely:

// WRONG: Only you understand it
const x = arr.map(a => a.p * (1 - d)).filter(b => b > 0);

// RIGHT: Everyone understands it
const applyDiscount = (price, discountPercent) => {
  return price * (1 - discountPercent / 100);
};

const filteredPrices = prices
  .map(price => applyDiscount(price, discount))
  .filter(price => price > 0);

In Shopify Context (Liquid):

<!-- WRONG: Magic numbers -->
{% if product.price > 10000 %}
  <span class="premium">Premium</span>
{% endif %}

<!-- RIGHT: Documented -->
{% comment %}
Premium products are those with price > $100 (10000 cents)
They get special treatment in search and collections
{% endcomment %}

{% assign premium_price_threshold = 10000 %}
{% if product.price > premium_price_threshold %}
  <span class="premium">Premium</span>
{% endif %}

Principle 3: One Responsibility per App/Component

The Single Responsibility Principle (SRP)

Wrong:

MyAwesomeApp handles:
- Discounts
- Bundling
- Inventory
- Analytics
- Custom Checkout

Every change affects everything!

Right:

DiscountApp handles:
- Creating discounts
- Applying discounts
- Reporting on discounts

(Only that!)

Principle 4: Document Decisions

# Decision Log

## Why do we use Metafields for Product Bundles?
**Date:** 2025-01-15
**Decision:** Use Metaobjects instead of separate App

**Alternatives considered:**
1. Custom App - Too heavyweight for this feature
2. Liquid only - Can't handle admin experience well

**Why Metaobjects?**
- Native Shopify (no external dependencies)
- Queryable in GraphQL (good DX)
- Admin UI auto-generated
- Can be related to Products

**Trade-offs:**
- Requires Shopify Plus (but we have it)
- Limited by Metaobject rate limits (ok for our use case)

**Who decided?** Claudio + Team Lead
**Status?** Implemented

This log helps new developers: "Why not do it differently?"

Principle 5: Write Tests (Early!)

Not at the end. From the very beginning.

// test-discount-logic.js
import { applyDiscount, validateCoupon } from './discount.js';

describe('Discount Logic', () => {
  it('should apply 10% discount correctly', () => {
    expect(applyDiscount(100, 10)).toBe(90);
  });

  it('should not allow negative discounts', () => {
    expect(() => applyDiscount(100, -10)).toThrow();
  });

  it('should validate coupon expiry', () => {
    const expiredCoupon = { code: 'OLD', expiresAt: '2024-01-01' };
    expect(validateCoupon(expiredCoupon)).toBe(false);
  });
});

When tests exist, you can safely refactor.

Paying Down Existing Debt

Step 1: Audit (Get the Facts)

# Measure code complexity
npm install complexity-report
complexity-report --format markdown . > complexity.md

# Find unused code
npm install unused-exports
unused-exports

# Check dependencies
npm audit

# Test coverage
npm test -- --coverage

Output:

  • Which files carry the most debt?
  • Which functions are too complex?
  • Which dependencies are outdated?

Step 2: Prioritize (What's Holding You Back the Most?)

Question: If you could refactor one file — which one would make features the fastest?

Example: Bekateq

Prioritized by: Impact on Velocity
1. Discount Logic (20% more complex than expected)
2. Variant Handling (10 different patterns)
3. Inventory Sync (5 different sources)

Step 3: Refactor with Safety

Don't just rewrite!

// SAFE refactoring:

// Step 1: Write tests (for the old code!)
// Step 2: Refactor (with tests green)
// Step 3: Deploy (tests protect you)
// Step 4: Monitor (is it still working?)
// Step 5: Delete old code (when everything is ok)

Example: Discount Logic Refactor

Before:
├─ discount.liquid (200 lines of spaghetti)
├─ discount.js (300 lines)
└─ discount.scss (100 lines)

Process:
├─ Step 1: Write tests for old code
├─ Step 2: Extract into functions
├─ Step 3: Deploy (tests protect)
├─ Step 4: Further extractions
├─ Step 5: Deploy
├─ Step 6: Delete old code

After:
├─ discount-logic.js (100 lines, testable)
├─ discount-component.jsx (50 lines)
└─ discount.scss (50 lines)

Cost-Benefit:

Refactoring: 2 weeks
Savings: 1 day per new feature (forever)
ROI: Break-even after 3 months

Step 4: Monitoring (You Will Accumulate Debt Again)

Metrics for Code Health:

- Code Coverage > 80%
- Complexity Score < 10 per function
- No outdated dependencies
- Zero security vulnerabilities

Best Practices from smplx.

1. Architecture Review (Before Every Major Feature)

Checklist:

  • Does this fit our existing architecture?
  • Do we need a new app or Liquid?
  • What are the risks?
  • How do we test this?
  • Who maintains this?

2. Code Review (Before Every Deploy)

## Review Checklist

- [ ] Tests green?
- [ ] No linting errors?
- [ ] Performance impact ok?
- [ ] Security review?
- [ ] Documentation updated?
- [ ] Old code removed?

3. Quarterly Debt Assessment

# Q1 2026 Technical Debt Report

## Debt: High
- Theme is 2 years old (Shopify 11 -> 12 needed)
- Discount Logic too complex (20+ patterns)
- No tests for Admin API Integration

## Investments needed:
1. Theme upgrade (2 weeks)
2. Refactor discounts (3 weeks)
3. Write tests (2 weeks)

## Cost-Benefit:
- Investment: 150 dev hours
- Savings: ~10 hours per month (new features)
- ROI: 15 months

The Reality: Debt Is OK

Important: You don't have to be debt-free!

Debt is OK when:

  • It's intentional (you know about it)
  • It's time-limited (plan for paydown)
  • It's not blocking you (features still work)

Debt is critical when:

  • It's hidden (nobody knows the code)
  • It has no exit strategy
  • It blocks new features
  • It constantly causes bugs

Checklist: Debt Prevention

  • Decisions documented
  • One App = One Purpose
  • Tests present
  • Code is readable
  • No 5+ apps for simple things
  • Theme is current (< 1 year old)
  • Dependencies current
  • Code review before deploy
  • Quarterly debt assessment
  • Clear refactoring plan

About the Author

Claudio Gerlich is Technical Architect at smplx. and has specialized in Shopify architecture since 2020. With Bekateq, we proved: Debt can be paid down, and it's worth it.

Technical debt is not a feature promise. It's your future, sold off.

smplx. is a Shopify Technical Partner (since 2020) based in Muensterland, NRW.