
Your MVP Is Not Your Product: The Technical Debt You Should Actually Keep
"Pay down your technical debt" is the most common advice in software engineering. It's also dangerously incomplete.
After building dozens of MVPs, we've learned that some technical debt is strategic. Cutting corners isn't laziness when it's deliberate, documented, and reversible. The goal of an MVP isn't clean code — it's validated learning. And validated learning has a deadline.
Here's our taxonomy of technical debt: what you keep, what you fix, and what never mattered in the first place.
Tier 1: Debt You Keep
This is intentional, time-boxed debt that lets you ship faster without compromising safety. Keep it until the product proves itself.
Hard-Coded Configuration
// "Debt" — but it ships today
const SUPPORTED_CURRENCIES = ["EUR", "USD"];
const MAX_ORDER_ITEMS = 50;
const FREE_SHIPPING_THRESHOLD = 50;
// "Clean" — but it ships next week
const config = await prisma.systemConfig.findMany();
const currencies = config.find(c => c.key === "supported_currencies")?.value;
// Plus: admin UI, validation, caching, migration...
A config table, admin interface, and caching layer to change a threshold that might never change? That's a week of work for a problem that doesn't exist yet. Hard-code it. Add a // TODO: make configurable if needed comment. Move on.
Basic Authentication
// MVP auth: email + password, session-based
// No OAuth, no SSO, no 2FA, no magic links
// It works. It's secure. It's not enterprise-ready.
Your MVP doesn't need "Sign in with Google," SAML SSO, or passwordless authentication on day one. Email and password with proper hashing is secure and takes hours to implement, not weeks.
Add OAuth when users ask for it. Add SSO when enterprise clients require it. Not before.
Manual Processes
Some things don't need to be automated yet:
- User onboarding: Manually create accounts for early users instead of building self-service signup with email verification, onboarding flows, and password recovery
- Invoicing: Generate invoices in a spreadsheet instead of integrating a billing system
- Reporting: Run SQL queries instead of building a dashboard
- Content updates: Edit code and redeploy instead of building a CMS
Automate something when you do it more than 10 times per week. Below that, manual is fine. You're an MVP — you probably have 20 users, not 20,000.
Simple Error Handling
// MVP: catch and log
try {
await processPayment(order);
} catch (error) {
console.error("Payment failed:", error);
return NextResponse.json(
{ error: "Payment failed. Please try again." },
{ status: 500 }
);
}
// Production: retry, fallback, alert, dead letter queue
// That's a two-week project. Build it when you have paying customers.
Tier 2: Debt You Must Fix Before Scaling
This is the dangerous category. It won't kill your MVP, but it will kill your product if you don't address it before you grow.
No Tests
An MVP with zero tests is fine for the first 4 weeks. An MVP that scales to a product with zero tests is a ticking time bomb.
When to add tests: Before you hire your second developer. Before you launch to the public. Before anyone's livelihood depends on this code not breaking.
What to test first: Not unit tests for utility functions. Test the critical paths:
// These are your first tests. Always.
describe("Critical paths", () => {
it("user can sign up and log in");
it("user can create an order");
it("payment processes successfully");
it("user receives confirmation email");
});
Four tests. They cover the revenue path. Everything else can wait.
No CI/CD
If deployment means "SSH into the server and run git pull," you're one bad merge away from downtime with no rollback plan.
Minimum viable CI/CD:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- run: npm test
- run: ./scripts/deploy.sh # rsync + docker compose up
This takes 2 hours to set up. It prevents entire categories of production incidents.
No Error Monitoring
If your app crashes and nobody gets an alert, did it really crash? Yes. It did. And your user left.
// Minimum: Sentry or equivalent
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1, // don't need full tracing for an MVP
});
15 minutes of setup. Free tier covers most MVPs. There's no excuse for not knowing when your app breaks.
We inherited a codebase that had been "running fine" for 3 months. It had a memory leak that caused the server to crash every 72 hours. The previous team's fix? A cron job that restarted the server every night. No error monitoring meant nobody knew why the restarts were necessary.
No Logging
console.log is not logging. You need structured logs that tell you what happened, when, and for whom:
// Not this
console.log("order created");
// This
logger.info("order_created", {
orderId: order.id,
userId: session.user.id,
total: order.total,
itemCount: order.items.length,
});
When something breaks in production, logs are the difference between "I can diagnose this in 10 minutes" and "I have no idea what happened."
Tier 3: Debt That Doesn't Matter
This is the counterintuitive part. Some "debt" that developers obsess over has zero impact on the product, the users, or the team's velocity.
Perfect Folder Structure
// Developers spend days debating this:
src/
├── modules/
│ ├── orders/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ ├── value-objects/
│ │ │ └── repositories/
│ │ ├── application/
│ │ │ ├── commands/
│ │ │ ├── queries/
│ │ │ └── handlers/
│ │ └── infrastructure/
│ ├── persistence/
│ └── mappers/
// When this works perfectly fine:
src/
├── app/
│ ├── orders/
│ │ ├── page.tsx
│ │ └── actions.ts
│ └── api/
│ └── orders/
│ └── route.ts
├── lib/
│ └── orders.ts
Clean Architecture, Hexagonal Architecture, Onion Architecture — these are valuable patterns for large, long-lived systems. For an MVP, they're premature abstraction. Use the framework's conventions. Refactor when the codebase grows to the point where conventions aren't enough.
100% TypeScript Strictness
// The full strict config:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true
}
}
strict: true is non-negotiable. The additional flags? They catch edge cases that matter in a million-line codebase but create friction in a 10,000-line MVP. Add them when the team grows.
Comprehensive Docstrings
If your function is called calculateOrderTotal and takes an Order, it doesn't need a JSDoc comment explaining that it calculates the order total. Name things well. Save documentation for the non-obvious.
Code Coverage Targets
"We need 80% code coverage" is a vanity metric. You can have 95% coverage and miss the one bug that loses customer data. Test the critical paths. Ignore the coverage number.
The Debt Audit Checklist
Before scaling your MVP into a product, run through this:
Must have (blocking):
- Critical path tests exist and pass
- CI/CD pipeline deploys automatically
- Error monitoring is active and alerting
- Structured logging is in place
- Database backups run automatically
- Secrets are in environment variables, not code
Should have (next sprint):
- Authentication handles edge cases (password reset, session expiry)
- Rate limiting on public endpoints
- Input validation on all API routes
- A README that lets a new developer run the project in 30 minutes
Nice to have (when you feel it):
- Admin dashboard for common operations
- Automated email workflows
- Performance monitoring
- Configurable business rules
The Bottom Line
Technical debt is a tool, not a sin. The question isn't "do we have debt?" — it's "do we know where it is, and do we know when to pay it off?"
Document your shortcuts. Make them deliberate. And never confuse an MVP with a product — the whole point of an MVP is to find out whether it should become one.

