Three months into my first app, I needed to add a simple feature: user roles (admin vs regular user). It took 40 hours to implement because I'd designed my database assuming everyone would have the same permissions. Future-proofing isn't predicting the future—it's building in flexibility for the changes you CAN'T predict.
Risk Radar: The 'I'll Fix It Later' Trap
The Mistake: Taking shortcuts because you're "just testing" or "will refactor later." Later never comes. That temporary hack becomes permanent the moment you have real users.
The Fix: Follow this rule: If it would take more than 4 hours to change later, build it properly now. If it's a 15-minute fix, you can afford to be scrappy.
Examples of 4+ hour changes: Database structure, authentication system, payment processing, file storage location
LLM Conversation Starter
Get your LLM to think about future scenarios:
I'm building [your app]. Currently planning to: - [Decision 1: e.g., store user data in this structure] - [Decision 2: e.g., use this authentication method] - [Decision 3: e.g., organize files this way] For each decision, tell me: 1. What will be HARD to change later? 2. What future features might this prevent? 3. Should I build it differently now? Be honest about tradeoffs. I'd rather spend an extra hour now than 20 hours refactoring later.
Why this works:
Your LLM can spot common pitfalls that take humans years to learn. This is preventive medicine for your codebase.
Abstraction: The Art of Smart Layers
Abstraction means hiding complexity behind simple interfaces. Done right, it means changing your entire payment processor by editing one file. Done wrong, it means adding unnecessary complexity that slows you down.
Good Abstraction Example
Scenario:
You're using Stripe for payments but might switch to PayPal later.
// lib/payment.ts
export async function processPayment(
amount: number,
userId: string
) {
// All Stripe-specific code here
return stripeCharge(amount, userId)
}
// In your app:
await processPayment(99.99, user.id)✅ To switch to PayPal, you only edit payment.ts. The rest of your app doesn't know or care.
Bad Abstraction Example
Scenario:
Stripe code scattered throughout your app.
// In 15 different files:
const stripe = require('stripe')
stripe.charges.create({
amount: 9999,
currency: 'usd',
// Stripe-specific setup
})❌ To switch to PayPal, you need to find and edit all 15 files. High chance of bugs.
PM Insight: The Vendor Switch Test
Professional teams ask: "If we had to switch vendors (payment, email, database host) tomorrow, how many files would we touch?"
Good abstraction: 1-3 files
Bad abstraction: 10+ files
This is called "dependency isolation" - keep external services contained in their own layer.
When to Abstract
Abstract These (High Change Risk)
- • Payment processing (might change providers)
- • Email sending (SendGrid vs Mailgun vs AWS SES)
- • File storage (local vs S3 vs Cloudflare R2)
- • Authentication (might add OAuth later)
- • Database queries (might optimize or change ORM)
Don't Abstract These (Low Change Risk)
- • UI components (you control them)
- • Simple calculations (unlikely to change)
- • One-off scripts (not used repeatedly)
Database Design That Scales
Database changes are the HARDEST to make later because they require data migration. Get these decisions right on day one, or pay the price with hours of migration scripts.
Database Golden Rules
1. Always Include These Fields
Even if you don't need them yet, you WILL need them eventually:
id // Unique identifier createdAt // When was this created? updatedAt // When was this last changed? deletedAt // For "soft deletes" (hide instead of remove)
2. Plan for Relationships
Think about how your data connects:
- • One user has many posts (one-to-many)
- • One post has many tags, tags belong to many posts (many-to-many)
- • One user has one profile (one-to-one)
3. Don't Store Calculated Data
❌ Bad:
Storing "totalPosts" count in User table (what if it gets out of sync?)
✅ Good:
Calculate on-demand: SELECT COUNT(*) FROM posts WHERE userId = X
4. Use Enums for Limited Choices
Instead of storing strings that could have typos:
// Instead of: role: "admin" (could be "Admin", "ADMIN", "administrator")
// Use enum:
enum UserRole {
ADMIN
USER
MODERATOR
}PM Insight: The Migration Cost Calculator
Real cost of database changes:
- • Write migration script: 1-3 hours
- • Test on development data: 1 hour
- • Backup production database: 30 minutes
- • Run migration on production: 15 minutes - 2 hours (depending on data size)
- • Fix bugs you didn't catch: 2-8 hours
- • Deal with data inconsistencies: 1-4 hours
Total: 6-18 hours for a "simple" database change. Worth spending an extra 30 minutes planning upfront.
Environment Variables (Secrets Management)
Hard-coding your database password in your code is like putting your house key under the doormat and posting a photo on Instagram. Environment variables keep secrets out of your codebase.
What Goes in Environment Variables
✅ Always Use Env Vars:
- • Database URLs/passwords
- • API keys (Stripe, OpenAI, etc.)
- • Authentication secrets
- • Email service credentials
- • Any production URLs
⚠️ Can Hardcode:
- • Public API URLs (like google.com)
- • UI text/labels
- • Colors/design tokens
- • Feature flags (for your own code)
Example .env file:
DATABASE_URL="postgresql://user:pass@host:5432/db" STRIPE_SECRET_KEY="sk_test_..." NEXTAUTH_SECRET="random-string-here" NEXTAUTH_URL="http://localhost:3000" # NEVER commit this file to GitHub! # Add .env to your .gitignore
Security Alert: The .env.example Pattern
Best practice: Create two files:
- • .env - Real secrets (NEVER commit to GitHub)
- • .env.example - Template with fake values (SAFE to commit)
# .env.example
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
STRIPE_SECRET_KEY="sk_test_your_key_here"
This helps collaborators know what variables they need to set up.
Dependency Management (Don't Break Your App)
Every library you install is a potential future problem. Not because libraries are bad, but because they update, break, or get abandoned. Choose dependencies carefully.
Safe Dependencies
Before adding a library, check:
- ✅ Updated in the last 6 months
- ✅ 1000+ GitHub stars (shows adoption)
- ✅ Good documentation
- ✅ Active issue responses
- ✅ Used by major companies
Warning Signs
Avoid libraries that:
- ❌ Haven't updated in 2+ years
- ❌ Have 100+ open issues
- ❌ Require 10+ other dependencies
- ❌ Have poor/missing docs
- ❌ Only have one maintainer
The 15-Minute Rule
Before installing any library, spend 15 minutes asking:
Could I build this myself in under 2 hours?
Simple date formatting? Write your own. Complex PDF generation? Use a library.
Am I using 10% or 90% of this library?
If you only need one function from a huge library, copy that function instead.
What happens if this library dies?
If the answer is "my entire app breaks," reconsider or abstract it properly.
PM Insight: The Dependency Audit Ritual
Professional teams do this quarterly: Review all dependencies and ask:
- • Are we still using this?
- • Is there a newer, better alternative?
- • Do any have security warnings?
- • Can we remove any?
Set a calendar reminder: "Dependency Audit" every 3 months. Takes 30 minutes, prevents hours of upgrade hell later.
Phase 4 Complete Checklist
"Code for the developer who has to maintain this in 6 months. That developer is you, and you will have forgotten everything."
– Your future self, wishing you'd planned better