Skip to main content

The Customization Tax: How Boilerplate Architecture Affects Long-Term Development

·StarterPick Team
customizationarchitecturetechnical-debtboilerplate2026

TL;DR

The customization tax is the ongoing cost of working around a boilerplate's architectural decisions. Some boilerplates have high taxes (everything is tightly coupled, changes ripple everywhere). Others have low taxes (clean separation of concerns, easy to modify). The purchase price matters less than the customization tax — a free boilerplate with high customization costs more over time than a $299 boilerplate with low customization costs.

Key Takeaways

  • Plugin architecture (Makerkit) = lowest customization tax
  • Monolithic architecture (most boilerplates) = medium tax
  • Tightly coupled = highest tax
  • The rule: Choose the simplest architecture that meets your needs
  • Long-term cost: High-tax boilerplates cost 2-4x more in developer time over 12 months

What Is the Customization Tax?

Customization Tax = Time spent understanding existing code
                  + Time adapting existing patterns to your needs
                  + Time working around architectural decisions
                  + Time maintaining customizations across updates

Every architectural decision in a boilerplate either reduces or increases this tax.


High Customization Tax: Tightly Coupled Architecture

Tightly coupled boilerplates have features that depend on each other in non-obvious ways:

// Tightly coupled: auth depends on billing depends on organizations
// You want to change how auth works — but it's tangled with billing

// auth/actions.ts
async function handleLogin(email: string, password: string) {
  const user = await authenticateUser(email, password);

  // Auth directly modifies subscription state — tangled!
  const subscription = await getOrCreateSubscription(user.id);
  if (subscription.status === 'canceled') {
    await sendCancellationEmail(user.email);
    throw new Error('Account canceled');
  }

  // Auth also handles org routing — tangled!
  const org = await getPrimaryOrganization(user.id);
  return { user, subscription, org, redirectTo: `/org/${org.slug}/dashboard` };
}

To change how login works (say, add SSO), you must understand and modify code that also handles billing and organization routing. Every change ripples.


Low Customization Tax: Clean Separation

// Clean: each concern is independent
// auth/actions.ts — ONLY handles auth
async function handleLogin(email: string, password: string) {
  const user = await authenticateUser(email, password);
  await createSession(user.id);
  return { userId: user.id };
}

// billing/actions.ts — ONLY handles billing
async function checkSubscriptionStatus(userId: string) {
  const subscription = await getSubscription(userId);
  return { hasAccess: subscription?.status === 'active' };
}

// The calling code coordinates them without tangling
async function loginAndRedirect(email: string, password: string) {
  const { userId } = await handleLogin(email, password);
  const { hasAccess } = await checkSubscriptionStatus(userId);
  const org = await getPrimaryOrg(userId);
  return { redirect: hasAccess ? `/org/${org.slug}` : '/pricing' };
}

Now you can change auth without touching billing, and vice versa.


Makerkit's Plugin Architecture

Makerkit uses a plugin architecture that minimizes customization tax:

// Makerkit: features as plugins
// packages/features/auth/src/index.ts — auth feature
export { AuthProvider } from './auth-provider';
export { useUser } from './use-user';
export { SignInForm } from './sign-in-form';

// packages/features/billing/src/index.ts — billing feature
export { BillingProvider } from './billing-provider';
export { PricingTable } from './pricing-table';
export { SubscriptionStatus } from './subscription-status';

// app/layout.tsx — compose features
import { AuthProvider } from '@kit/auth';
import { BillingProvider } from '@kit/billing';

export default function RootLayout({ children }) {
  return (
    <AuthProvider>
      <BillingProvider>
        {children}
      </BillingProvider>
    </AuthProvider>
  );
}

To replace the auth provider, you only touch the auth package. Billing is untouched. This is the minimum customization tax.


Architectural Patterns and Their Tax Rate

Pattern 1: Service Layer (Medium-Low Tax)

// Makerkit, Supastarter use service layer pattern
// app/services/user.service.ts
export class UserService {
  constructor(private db: PrismaClient) {}

  async getUser(id: string) {
    return this.db.user.findUnique({ where: { id } });
  }

  async updateUser(id: string, data: Partial<User>) {
    return this.db.user.update({ where: { id }, data });
  }
}

// To add a new behavior: extend the service, don't touch callers

Customization tax: Add methods to the service. Changes are isolated.

Pattern 2: Direct Database Calls (Medium Tax)

// ShipFast, T3 Stack: direct Prisma calls throughout
// lib/user.ts
export async function getUser(id: string) {
  return prisma.user.findUnique({ where: { id } });
}

// To add caching: update lib/user.ts, update all callers
// Changes ripple but are predictable

Customization tax: Find all usages with grep, update each. More work but straightforward.

Pattern 3: ORM Everywhere (High Tax)

// Pattern to avoid: direct ORM calls scattered throughout the app
// app/dashboard/page.tsx
const user = await prisma.user.findUnique({ where: { id: session.userId }, include: { subscription: true } });

// app/settings/page.tsx
const user = await prisma.user.findUnique({ where: { id: session.userId }, include: { subscription: true } });

// app/billing/page.tsx
const user = await prisma.user.findUnique({ where: { id: session.userId }, include: { subscription: true } });

To change the user query (add caching, add a new field), you must update every page. This is maximum customization tax.


Measuring Your Boilerplate's Tax Rate

Before buying, open the codebase and answer:

  1. Can I change the auth provider without touching billing code?

    • Yes → Low tax
    • No → Medium-High tax
  2. If I add a new database field, how many files change?

    • 1-3 files → Low tax
    • 4-10 files → Medium tax
    • 10+ files → High tax
  3. Is there a clear pattern for adding new features?

    • Yes, with examples → Low tax
    • No pattern → High tax
  4. Are the largest files < 200 lines?

    • Yes → Low tax (well-factored)
    • No → High tax (tightly coupled)

Long-Term Cost by Architecture

Architecture6-Month Add CostNotes
Plugin (Makerkit)LowChanges stay contained
Service LayerMediumChanges predictable
Direct callsMedium-HighWidespread updates needed
Tightly coupledHighRipple effects everywhere

Over a 12-month product development cycle, architecture choice can mean 50-100 extra developer hours in a medium-complexity product.


The Upgrade Tax

When the boilerplate releases updates, high-tax architectures cause merge conflicts everywhere:

# Low-tax boilerplate update
git pull origin main
# 3 files changed, 45 insertions(+), 12 deletions(-)
# Your customizations are in different files → no conflicts

# High-tax boilerplate update
git pull origin main
# CONFLICT (content): Merge conflict in app/api/auth.ts
# CONFLICT (content): Merge conflict in lib/stripe.ts
# CONFLICT (content): Merge conflict in app/dashboard/page.tsx
# 12 files conflict → 2 hours resolving

Low-tax architectures let you stay updated with minimal friction. High-tax architectures make updates painful enough that teams stop updating — accumulating security debt.


Find boilerplates with clean architectures that minimize your customization tax on StarterPick.

Check out this boilerplate

View Makerkit on StarterPick →

Comments