Skip to main content

The Boilerplate Trap: When Pre-Built Code Becomes Technical Debt

·StarterPick Team
technical-debtboilerplaterefactoringsaas2026

TL;DR

The boilerplate trap happens when you've built so much on top of a poorly-fitting foundation that changing the foundation requires rewriting the product. It's the most expensive mistake in early-stage SaaS development. Signs: you're fighting the boilerplate daily, workarounds outnumber features, and every new feature requires understanding old boilerplate code first.

Key Takeaways

  • The trap usually springs at 3-6 months of active development
  • Early warning signs are subtle — easy to rationalize as "normal friction"
  • The cost is highest if ignored — it compounds with every feature added
  • Escape routes exist — selective replacement without full rewrite
  • Prevention is cheaper than cure — right boilerplate selection matters

What the Boilerplate Trap Looks Like

Three months into development, a team discovers:

Week 12 progress report:
- Feature planned: User organization system
- Actual time: 2x estimate
- Reason: Had to understand and work around ShipFast's user model
  to add organization relationships

Week 16 progress report:
- Feature planned: Per-organization billing
- Actual time: 3x estimate
- Reason: ShipFast's billing model assumes per-user, not per-organization.
  Had to refactor 8 files.

Week 20 progress report:
- Feature planned: Member invitation system
- Actual time: 4x estimate
- Reason: Organization system we built in Week 12 doesn't integrate
  with ShipFast's email system. Had to bridge them.

Each feature is harder than the last. The boilerplate's architecture is fighting the product's requirements at every step.


The Three Stages of the Trap

Stage 1: Friction (Weeks 1-8)

// "This is just a minor annoyance"
// ShipFast uses userId-centric billing, but you want organization billing
// You add a workaround:

type BillingEntity = User & {
  organizationId?: string; // Hack: store org info on user
  isOrgBilling: boolean;
};

// "It's fine, it works"

Stage 1 is manageable. The workaround feels small.

Stage 2: Debt Accumulation (Weeks 8-20)

// Workarounds multiply
// lib/billing.ts
async function getSubscriptionStatus(userId: string) {
  const user = await db.user.findUnique({ where: { id: userId } });

  // Workaround 1: Check if user is in an org-billing organization
  if (user?.organizationId && user?.isOrgBilling) {
    const org = await db.organization.findUnique({ where: { id: user.organizationId } });
    return org?.subscription?.status ?? 'inactive';
  }

  // Workaround 2: Fall back to user-level billing
  return user?.subscription?.status ?? 'inactive';
}

// api/auth/route.ts
async function login(email: string, password: string) {
  const user = await authenticateUser(email, password);
  // Workaround 3: Determine billing entity for session context
  const billingEntity = user.isOrgBilling
    ? await getOrgBillingContext(user)
    : user;

  // Workaround 4: Handle both billing paths in session
  const session = createSession({
    ...user,
    hasAccess: await checkAccess(billingEntity),
  });
}

You're now maintaining parallel code paths for the boilerplate's model and your model.

Stage 3: The Trap (Weeks 20+)

// Every new feature touches the billing/auth bridge
// New engineer joins: "Why do we have two billing paths?"
// Answer: "It's complicated, don't touch it"

// New feature: Team member permissions
// You need to check: is user in org? what's org billing status?
// what permissions does this member have? but wait, some users are
// on user-level billing...

// 3 days to understand before writing the feature
// 5 days to write the feature
// 2 days to test edge cases
// vs: 2 days if the boilerplate supported org billing from the start

You're now spending 4x the time on features because of accumulated workarounds.


Early Warning Signs

Recognize the trap before it fully springs:

Week 4-8 warning signs:

  • You've added a [feature]_v2 file alongside a boilerplate file
  • You have "don't touch this" comments in the codebase
  • A new developer can't understand a core feature without a tour
  • More than 30% of your code is "plumbing" between your product and the boilerplate

Week 8-16 warning signs:

  • Feature estimates are consistently 2x+ what they should be
  • Every new feature requires changes in auth, billing, OR both
  • You've written tests for your workarounds (workarounds have tests!)
  • The boilerplate's update notes are irrelevant to your codebase

Escape Routes

If you recognize you're in the trap, you have options:

Option 1: Selective Extraction (Best)

Identify the 1-2 modules causing the most friction and replace them:

# Example: Replace the user-centric billing with org-centric billing
# Without rewriting auth, email, or UI

# 1. Create new billing module
mkdir lib/billing-v2
# 2. Implement org-centric billing cleanly
# 3. Create adapter layer between old and new
# 4. Migrate API routes one at a time
# 5. Delete old billing code when all routes migrated

Timeline: 2-3 weeks for one module. Product continues shipping during migration.

Option 2: Wrapper Layer

// Create an abstraction that hides the boilerplate's model
// from your product code

// billing/index.ts — your abstraction
export async function getBillingStatus(context: BillingContext): Promise<BillingStatus> {
  // Internally handles both user-billing and org-billing paths
  // Product code only calls this, never the underlying boilerplate billing
  if (context.type === 'organization') {
    return getOrgBillingStatus(context.organizationId);
  }
  return getUserBillingStatus(context.userId);
}

// Product code
const status = await getBillingStatus({ type: 'organization', organizationId });
// No boilerplate implementation detail exposed

Option 3: Migrate to a Better-Fit Boilerplate

If the architectural mismatch is fundamental (you chose ShipFast but need multi-tenancy throughout):

Timeline: 2-4 weeks of partial rewrite
Approach:
1. Get new boilerplate working
2. Port your unique business logic (not the infrastructure)
3. Use feature flags to migrate users

This is expensive but worth it if Stage 3 trap is already sprung.

Option 4: The Real Full Rewrite (Last Resort)

Sometimes the product has grown beyond any boilerplate:

Signs you've outgrown boilerplates:
- Custom auth requirements (SAML, LDAP, custom token format)
- Revenue > $500k ARR (infrastructure investment is justified)
- Platform product (you ARE the auth/billing for others)
- 5+ engineers on the team

At this point, build your own internal "boilerplate" based on what you've learned.


Prevention: The Right Way

The trap is 100% preventable with the right boilerplate selection:

Before buying any boilerplate, answer:
1. Does my product need multi-tenancy? → Use Makerkit/Supastarter if yes
2. Does my product need custom billing? → Verify boilerplate supports it
3. Does my product need X? → Check if boilerplate has X before buying

The 30 minutes spent evaluating a boilerplate before buying prevents weeks of trap-escaping.


Use StarterPick's detailed feature comparison to avoid the boilerplate trap at StarterPick.

Check out this boilerplate

View Makerkit on StarterPick →

Comments