Sustainable Code
Why This Matters
Your product is live. Users are signing up. Features are shipping. Everything is working, for now.
But under the hood, you’ve been making tradeoffs. That function you copy-pasted because there wasn’t time to refactor. The API route that handles five different things. The component that’s 800 lines long. The tests you meant to write.
This is technical debt, and every product accumulates it. This chapter teaches you to recognize tech debt, decide when to pay it down, and implement practices that keep your codebase sustainable as you scale.
Part 1: The Strategy
What is Technical Debt?
Technical debt is the implicit cost of additional rework caused by choosing an expedient solution now instead of using a better approach that would take longer.
The metaphor comes from finance: just like financial debt, technical debt: - Accumulates interest: The longer you wait to pay it down, the more expensive it becomes - Can be strategic: Sometimes borrowing makes sense - Can be crippling: Too much debt slows everything down
Examples of technical debt: - Duplicated code across multiple files - Hard-coded values that should be configurable - Missing tests for critical functionality - Components that do too many things - Outdated dependencies with security vulnerabilities - No error handling for edge cases
Strategic vs. Accidental Debt
Not all tech debt is bad. Some is intentional and strategic.
Strategic (Deliberate) Debt
When it makes sense: - Racing to validate product-market fit - Meeting a hard deadline (demo, investor pitch) - Testing a hypothesis quickly - Competitive pressure requires speed
Example: > “We know this data model won’t scale past 10,000 users, but we need to launch next week to test demand. If it works, we’ll rebuild the database layer.”
The key: You know you’re taking on debt, and you have a plan to address it.
Accidental (Inadvertent) Debt
How it happens: - Developers didn’t know a better approach - Requirements changed after implementation - Quick fixes that became permanent - Poor communication about system design
Example: > “I didn’t realize there was already a date formatting utility, so I wrote another one. Now we have three different date formatters and they don’t all produce the same output.”
The Tech Debt Quadrant
| Reckless | Prudent | |
|---|---|---|
| Deliberate | “We don’t have time for tests” | “Ship now, refactor later” |
| Inadvertent | “What’s a design pattern?” | “Now we know how we should have built it” |
Prudent deliberate debt is often justified. It’s a conscious business decision.
Reckless inadvertent debt is the most dangerous, you don’t even know you’re accumulating it.
When to Pay Down Tech Debt
The answer isn’t “always” or “never”. It depends on context.
Pay it down when: - The debt is blocking new feature development - You’re about to modify code in that area anyway - It’s causing bugs or performance issues - Onboarding new developers is difficult - Security vulnerabilities are involved
Defer payment when: - You’re still validating product-market fit - The code might be deleted soon (pivoting) - The cost of refactoring exceeds the benefit - You have hard deadlines with real consequences
The Boy Scout Rule: Leave the code better than you found it. When you’re working in an area, clean up a little. Incremental improvements compound over time.
Growth and Retention Strategy
As your product matures, focus shifts from acquisition to retention. Sustainable code enables sustainable growth.
The User Lifecycle
Awareness → Acquisition → Activation → Retention → Revenue → Referral
↑
You are here
(Sprint 3-4 focus)
By this point, you should have: - Acquisition: Users can find and sign up - Activation: Users experience core value
Now focus on: - Retention: Users keep coming back - Revenue: Users are willing to pay - Referral: Users tell others
Retention Levers
| Lever | Description | Technical Implementation |
|---|---|---|
| Habit formation | Make the app part of daily routine | Push notifications, email reminders |
| Increased value | More usage = more value | Data accumulation, personalization |
| Social connection | Users connect with others | Sharing, collaboration features |
| Switching costs | Leaving means losing something | Data export friction (ethical limits) |
Onboarding Optimization
The first experience determines whether users return.
Good onboarding: - Gets users to the “aha moment” fast - Teaches by doing, not explaining - Celebrates small wins - Removes friction ruthlessly
Common mistakes: - Too many steps before value - Requiring unnecessary information - Information overload - No guidance after signup
Measure it: - Time to first value action - Completion rate of onboarding steps - Drop-off points in the flow - Day 1 retention vs. users who completed onboarding
Part 2: Building It
Code Quality Fundamentals
The DRY Principle
DRY (Don’t Repeat Yourself) means each piece of knowledge should exist in exactly one place.
Before (repetitive):
// Three places with the same logic
function validateStudentDiscount(user) {
if (!user.email) return false;
return user.email.endsWith('.edu');
}
function calculateStudentPrice(price, user) {
if (!user.email) return price;
if (user.email.endsWith('.edu')) {
return price * 0.8;
}
return price;
}
function showStudentBanner(user) {
if (user.email && user.email.endsWith('.edu')) {
return true;
}
return false;
}After (DRY):
// One source of truth
function isStudent(user) {
return user.email?.endsWith('.edu') ?? false;
}
function validateStudentDiscount(user) {
return isStudent(user);
}
function calculateStudentPrice(price, user) {
return isStudent(user) ? price * 0.8 : price;
}
function showStudentBanner(user) {
return isStudent(user);
}Single Responsibility
Each function, component, or module should do one thing.
Before (doing too much):
function handleUserSubmit(formData) {
// Validate
if (!formData.email) throw new Error('Email required');
if (!formData.name) throw new Error('Name required');
// Transform
const user = {
email: formData.email.toLowerCase(),
name: formData.name.trim(),
createdAt: new Date()
};
// Save
await supabase.from('users').insert(user);
// Send email
await sendWelcomeEmail(user.email);
// Track
await trackEvent('user_signed_up', { email: user.email });
// Redirect
router.push('/dashboard');
}After (single responsibility):
async function handleUserSubmit(formData) {
const validatedData = validateUserForm(formData);
const user = await createUser(validatedData);
await onUserCreated(user);
router.push('/dashboard');
}
function validateUserForm(formData) {
if (!formData.email) throw new Error('Email required');
if (!formData.name) throw new Error('Name required');
return formData;
}
async function createUser(formData) {
const user = transformUserData(formData);
await supabase.from('users').insert(user);
return user;
}
async function onUserCreated(user) {
await Promise.all([
sendWelcomeEmail(user.email),
trackEvent('user_signed_up', { email: user.email })
]);
}Linting and Formatting
Automated tools catch problems and maintain consistency without manual effort.
ESLint Setup
npm install -D eslint @eslint/js
npx eslint --initBasic .eslintrc.json:
{
"extends": ["next/core-web-vitals", "eslint:recommended"],
"rules": {
"no-unused-vars": "warn",
"no-console": "warn",
"prefer-const": "error"
}
}Prettier Setup
npm install -D prettier eslint-config-prettierCreate .prettierrc:
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}Add to package.json:
{
"scripts": {
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}Testing Fundamentals
Tests catch bugs before users do and give you confidence to refactor.
Types of Tests
| Type | What It Tests | Speed | Coverage |
|---|---|---|---|
| Unit | Individual functions | Fast | Narrow |
| Integration | Components working together | Medium | Medium |
| End-to-End | Full user flows | Slow | Broad |
The testing pyramid: Lots of unit tests, fewer integration tests, even fewer E2E tests.
Setting Up Jest
npm install -D jest @testing-library/react @testing-library/jest-domCreate jest.config.js:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1'
}
};Writing Your First Tests
// lib/utils.test.js
import { formatCurrency, calculateDiscount } from './utils';
describe('formatCurrency', () => {
it('formats positive numbers correctly', () => {
expect(formatCurrency(1234.56)).toBe('$1,234.56');
});
it('handles zero', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
it('handles negative numbers', () => {
expect(formatCurrency(-50)).toBe('-$50.00');
});
});
describe('calculateDiscount', () => {
it('applies percentage discount', () => {
expect(calculateDiscount(100, 20)).toBe(80);
});
it('returns original price for zero discount', () => {
expect(calculateDiscount(100, 0)).toBe(100);
});
it('handles 100% discount', () => {
expect(calculateDiscount(100, 100)).toBe(0);
});
});Testing React Components
// components/Counter.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
describe('Counter', () => {
it('renders initial count', () => {
render(<Counter initialCount={5} />);
expect(screen.getByText('Count: 5')).toBeInTheDocument();
});
it('increments when + clicked', () => {
render(<Counter initialCount={0} />);
fireEvent.click(screen.getByText('+'));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('decrements when - clicked', () => {
render(<Counter initialCount={5} />);
fireEvent.click(screen.getByText('-'));
expect(screen.getByText('Count: 4')).toBeInTheDocument();
});
});Refactoring Patterns
Refactoring is improving code structure without changing behavior.
Extract Function
When code does too many things, extract pieces into separate functions.
Before:
function processOrder(order) {
// Calculate total
let total = 0;
for (const item of order.items) {
total += item.price * item.quantity;
}
// Apply discount
if (order.discountCode === 'SAVE20') {
total *= 0.8;
}
// Add tax
total *= 1.08;
// Format receipt
return `Order Total: $${total.toFixed(2)}`;
}After:
function processOrder(order) {
const subtotal = calculateSubtotal(order.items);
const afterDiscount = applyDiscount(subtotal, order.discountCode);
const withTax = addTax(afterDiscount);
return formatReceipt(withTax);
}
function calculateSubtotal(items) {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
function applyDiscount(amount, code) {
const discounts = { 'SAVE20': 0.8, 'SAVE10': 0.9 };
return amount * (discounts[code] || 1);
}
function addTax(amount, rate = 0.08) {
return amount * (1 + rate);
}
function formatReceipt(total) {
return `Order Total: $${total.toFixed(2)}`;
}Extract Component
When a React component is too large, split it into smaller pieces.
Before (500+ line component):
function Dashboard() {
// 50 lines of state
// 100 lines of data fetching
// 200 lines of handlers
// 150 lines of JSX
}After:
function Dashboard() {
return (
<DashboardLayout>
<DashboardHeader />
<MetricsGrid />
<RecentActivity />
<QuickActions />
</DashboardLayout>
);
}Supabase Realtime
Realtime features keep users engaged and create stickiness.
Setting Up Realtime
Enable realtime on your table:
-- Enable realtime for a table
ALTER PUBLICATION supabase_realtime ADD TABLE messages;Or in the Supabase dashboard: Table Editor → Your Table → Enable Realtime
Subscribing to Changes
import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';
function MessageList({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// Initial fetch
async function fetchMessages() {
const { data } = await supabase
.from('messages')
.select('*')
.eq('room_id', roomId)
.order('created_at');
setMessages(data || []);
}
fetchMessages();
// Subscribe to new messages
const subscription = supabase
.channel(`room:${roomId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `room_id=eq.${roomId}`
},
(payload) => {
setMessages(prev => [...prev, payload.new]);
}
)
.subscribe();
// Cleanup on unmount
return () => {
subscription.unsubscribe();
};
}, [roomId]);
return (
<ul>
{messages.map(msg => (
<li key={msg.id}>{msg.content}</li>
))}
</ul>
);
}Presence: Who’s Online
Show who else is viewing the same content:
function PresenceIndicator({ roomId, currentUser }) {
const [onlineUsers, setOnlineUsers] = useState([]);
useEffect(() => {
const channel = supabase.channel(`presence:${roomId}`);
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState();
const users = Object.values(state).flat();
setOnlineUsers(users);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: currentUser.id,
user_name: currentUser.name,
online_at: new Date().toISOString()
});
}
});
return () => {
channel.unsubscribe();
};
}, [roomId, currentUser]);
return (
<div className="flex gap-2">
{onlineUsers.map(user => (
<span key={user.user_id} className="px-2 py-1 bg-green-100 rounded">
{user.user_name}
</span>
))}
</div>
);
}Realtime Use Cases
| Feature | How It Works |
|---|---|
| Live chat | Subscribe to message inserts |
| Collaborative editing | Broadcast cursor positions with presence |
| Notifications | Subscribe to notifications table |
| Live dashboards | Subscribe to metrics table updates |
| Activity feeds | Subscribe to activity inserts |
This Week’s Sprint Work
This is build week, dedicated time to polish and improve.
Focus areas:
- Address critical bugs from user testing
- Improve onboarding based on where users dropped off
- Add at least one test for your core functionality
- Clean up one area of tech debt you’ve been avoiding
- Optional: Add a realtime feature if it makes sense for your product
Tech Debt Audit:
Use Claude Code to audit your codebase:
claudeReview my codebase and identify: 1. Duplicated code that should be consolidated 2. Large files that should be split 3. Missing error handling 4. Potential security issues 5. Code that’s hard to understand
Prioritize by impact and effort required.
Key Concepts
- Technical Debt: Cost of rework from choosing quick solutions over better ones
- Strategic Debt: Deliberate shortcuts with a plan to address them
- Accidental Debt: Unintentional problems from lack of knowledge
- Boy Scout Rule: Leave code better than you found it
- DRY (Don’t Repeat Yourself): Single source of truth for each piece of knowledge
- Single Responsibility: Each function/component does one thing
- Linting: Automated code quality checks
- Formatting: Consistent code style
- Unit Tests: Testing individual functions
- Integration Tests: Testing components working together
- Refactoring: Improving structure without changing behavior
- Extract Function/Component: Breaking large pieces into smaller ones
- Supabase Realtime: Live database subscriptions
- Presence: Tracking who’s currently online