Be prepared to throw away your code
Why the best architecture might be the one you're willing to throw away
I used to think good code was code that lasted forever.
Elegant abstractions. Perfect separation of concerns. The kind of architecture that would make senior engineers nod approvingly during code reviews I’d never actually have.
So I’d spend hours future-proofing everything.
Designing plugin systems for features that didn’t exist yet. Building configuration layers for options nobody asked for. Creating abstractions so flexible they could handle anything — except the one thing I actually needed to ship.
Then one day, I had to rip out an entire auth system I’d spent two weeks perfecting.
Not because it was broken. Because the product changed direction and suddenly we needed OAuth instead of email/password.
All those beautiful interfaces, those carefully crafted base classes, those thoughtful error hierarchies — deleted in an afternoon.
And you know what hurt most? It wasn’t the wasted time.
It was how hard it was to delete.
The auth logic had tendrils everywhere. Every controller imported it. Every model referenced it. Every test depended on it. Removing it felt like performing surgery on a patient who was still awake.
That’s when I learned the most important lesson about indie dev architecture:
The best code isn’t the code that lasts forever. It’s the code that’s easy to throw away.
The Permanence Trap
We’re taught to write code like we’re building cathedrals.
Solid foundations. Careful planning. Structure that will stand for generations.
But indie SaaS isn’t a cathedral. It’s a sandcastle at high tide.
The market shifts. Users want something different. You realize your big idea was actually three smaller ideas wearing a trench coat.
If your code assumes permanence, change becomes painful. Every pivot feels like demolition. You’ll avoid necessary changes just because the refactor is too scary.
What Disposable Design Actually Looks Like
This doesn’t mean writing sloppy code. It means writing code that’s easy to replace when you inevitably need to.
Here’s a real example from my own codebase.
The permanent version (what I used to write):
class AuthenticationService:
def __init__(self, token_provider, session_manager, audit_logger):
self.token_provider = token_provider
self.session_manager = session_manager
self.audit_logger = audit_logger
def authenticate(self, credentials):
# Complex authentication logic spanning 50 lines
# with calls to all the injected dependencies
pass
def refresh_token(self, old_token):
# More complex logic intertwined with session management
pass
def validate_session(self, session_id):
# Even more logic that assumes this exact architecture
passNow imagine trying to swap this out for OAuth. You’d need to:
Find every place that imports
AuthenticationServiceUnderstand what each method does and how they’re used
Figure out which of your abstractions still make sense
Keep the whole system working while you rebuild it
The disposable version (what I write now):
def login_user(email, password):
“”“Log in with email and password. Returns user_id or None.”“”
user = db.query(”SELECT * FROM users WHERE email = ?”, email)
if user and check_password(password, user.password_hash):
session_id = create_session(user.id)
return user.id
return None
def create_session(user_id):
“”“Create a session for user. Returns session_id.”“”
session_id = generate_token()
db.execute(”INSERT INTO sessions (id, user_id) VALUES (?, ?)”,
session_id, user_id)
return session_idLook at the difference. No grand abstractions. No injection hierarchies. Just small functions that do one thing.
When I needed to switch to OAuth? I wrote new functions:
def login_with_google(oauth_code):
“”“Exchange Google OAuth code for user session.”“”
google_user = fetch_google_user(oauth_code)
user_id = get_or_create_user(google_user.email)
return create_session(user_id)Notice something? I reused create_session because it was generic enough. But login_user? Deleted. Gone. Didn’t even hesitate.
No refactoring. No careful extraction. Just wrote the new thing and removed the old thing.
The Rules of Disposable Code
1. Small, obvious functions over big, clever classes
Classes accumulate dependencies. Functions are isolated. When you need to delete a function, you delete a function. When you need to delete a class, you delete a class and everything tangled up with it.
2. Duplication is cheaper than the wrong abstraction
I used to obsess over DRY (Don’t Repeat Yourself). Now I’m fine with a little repetition if it keeps things independent.
Two similar functions are easier to replace than one abstraction that tries to handle both cases.
3. Keep your interfaces at the boundary, not everywhere
You don’t need an interface for your database layer when you’re the only one touching it. You can always add it later when you have a reason.
Interfaces make sense at system boundaries — the edge where your code meets the world. Everywhere else, they’re just ceremony.
4. Write code that explains itself, not code that needs explanation
When something’s easy to delete, you need to understand it quickly. Clear names, simple flows, minimal indirection.
If you need to draw a diagram to explain how your authentication works, it’s probably too coupled to delete easily.
I know this is easier said than done, but it gets better as you consciously repeat it.
When Permanence Actually Matters
I’m not saying nothing should last.
Your database schema? That’s hard to change, so think it through.
Your user-facing API? Breaking that hurts customers, so be careful.
But your internal implementation? Your service layer? Your clever abstraction that makes you feel like a real engineer?
That stuff should be built like lego blocks, not concrete.
The Real Discipline
Here’s the paradox: writing disposable code takes more discipline than writing permanent code.
Permanent code lets you over-engineer. You can spend days on an abstraction and call it “planning ahead.”
Disposable code forces you to admit you don’t know the future. You solve today’s problem cleanly, knowing tomorrow might demand something different.
That’s harder. It requires restraint. It means resisting the urge to build the Grand Unified Framework when a simple function would do.
But it’s also freeing.
Because when the product changes — and it will — you won’t be buried under the weight of all your clever decisions.
You’ll just delete the old thing and write the new thing.
And keep shipping.
TL;DR:
Stop building for forever. Build for now, with the assumption that “now” is temporary. The best codebases aren’t the ones that never change — they’re the ones where change doesn’t hurt.

