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
Time Pressure: "We need this feature by Friday!" -> Quick & dirty solution -> Tomorrow a bug -> Patch on top -> After 6 months: Unmaintainable
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
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
Outdated Themes: "Let's keep the old theme" -> Old theme version -> New features not possible -> Missing security updates -> After 2 years: Breakage
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.