How to Stop Overengineering Side Projects (You Don't Need That Abstraction Layer)

You Are Not Building Infrastructure. You Are Building a Product.
Last year I spent three weeks building a custom event system for a side project. It had a publisher-subscriber pattern, typed events, middleware support, and a replay mechanism for debugging. It was clean. It was extensible. It was, objectively, well-engineered.
The project had zero users. It had four event types. A simple function call would have worked fine.
This is overengineering, and if you are a developer working on side projects, you have done it too. Maybe you built a custom ORM instead of using raw queries. Maybe you created a plugin architecture for a product that has no plugins. Maybe you spent a week setting up a microservices architecture for an app that could run perfectly well as a single Express server.
The question of how to stop overengineering side projects is really a question about identity. We are engineers. Engineering things is what we do. And when the boring work of building basic CRUD features sits next to the interesting work of designing elegant abstractions, our brains choose the interesting work every single time. Not because we are lazy. Because we are wired to solve hard problems, and sometimes we manufacture hard problems to solve when the real work is too simple to be satisfying.
Why We Overengineer (And Why It Feels Productive)
There are three reasons developers overengineer side projects, and understanding them is the first step toward stopping.
The first reason is that overengineering feels like progress. You are writing code. You are solving problems. You are making architectural decisions. All of that feels like building. The issue is that you are building the wrong thing. You are building infrastructure for a product that does not exist yet, and infrastructure without a product is just practice.
I have had coding sessions where I felt incredibly productive, wrote hundreds of lines, solved a genuinely tricky design problem, and at the end of the session realized that none of what I built was visible to a user. The abstraction layer was invisible. The type system was invisible. The event bus was invisible. From a user's perspective, I had accomplished nothing. But from my perspective, it felt like a great day of work. That disconnect is the trap.
The second reason is that overengineering is technically interesting. Building the basic features of a side project is often boring. Another form. Another database table. Another CRUD endpoint. You have done this a hundred times. There is no novelty in it. But designing a flexible configuration system? Implementing a custom caching layer? Building a type-safe API client generator? That is fun. That is the kind of work that makes you feel like a "real" engineer.
The problem is that fun and progress are not the same thing. You can have a wonderful time building an in-memory caching layer for your side project, but that caching layer is not getting your product in front of users. The boring CRUD work is. And every hour you spend on the interesting-but-unnecessary work is an hour stolen from the boring-but-necessary work. This pattern connects directly to programmer perfectionism — the drive to make things elegant rather than done. Recognizing when you have crossed that line is its own skill.
The third reason is fear. Specifically, fear of technical debt. Developers who have worked on large codebases know the pain of dealing with shortcuts that someone took three years ago. The hardcoded value that is now in 47 places. The function that grew to 500 lines because nobody refactored it. The database schema that no longer makes sense but is too entangled to change. This is real pain, and the instinct to avoid it is rational.
But it is wrong for side projects. Here is why: the technical debt you fear will cause pain only if your project succeeds. If your project never ships, or ships and gets no users, the technical debt is irrelevant because nobody will ever encounter it. You are optimizing for a future that might not happen while sacrificing the present where you need to ship. This is what Martin Fowler calls YAGNI — You Aren't Gonna Need It. Every feature and every abstraction you build "just in case" carries a cost. For a side project, that cost is almost never worth paying upfront.
The probability-weighted cost of overengineering is almost always higher than the probability-weighted cost of technical debt, because the probability of shipping drops with every unnecessary abstraction you add.
Signs You Are Overengineering
If you are not sure whether you are overengineering, here are some diagnostic questions. Answer honestly.
Are you spending more time on architecture than features? If the ratio of time spent designing the system to time spent building what users see is higher than 1:3, you are probably overengineering. Architecture serves features, not the other way around.
Are you building for users you do not have? If you are designing for "thousands of concurrent users" and you currently have zero, you are overengineering. Build for 10 users. Literally 10. The architecture that serves 10 users is dramatically simpler than the architecture that serves 10,000, and if you ever get to 10,000, you will rewrite most of the code anyway because your assumptions about usage patterns were wrong.
Are you creating abstractions before you have a second use case? The rule of three exists for a reason. You do not create an abstraction the first time you write something. You do not create it the second time you write something similar. You create it the third time, because by then you actually understand the pattern and can abstract it correctly. If you are building abstractions on the first use case, you are guessing at what the abstraction should look like, and you will guess wrong.
Are you writing configuration systems instead of hardcoding values? A configuration file for a side project with one deployment is overhead. Hardcode the values. If you later need to change them, a find-and-replace takes thirty seconds. The configuration system you are building to "make changes easier" takes hours and solves a problem you will encounter for thirty seconds at most.
Can you explain your architecture to a non-developer without it sounding absurd? "I have a database and a server and a frontend" makes sense to anyone. "I have a message queue that publishes events to a series of microservices that each manage their own data store" does not make sense for a todo app, and the reason it does not make sense is that it is overengineered.
Does This Help Me Ship?
Before adding any architectural element to your side project, ask one question: does this help me ship?
Not "is this good practice?" Not "would this scale well?" Not "would a senior developer approve?" Just: does building this get me closer to a shipped product, or further away?
If you cannot answer yes immediately, the answer is no. Put it on a post-launch list and move on. This single filter eliminates most overengineering decisions before they cost you hours.
Apply it to specific cases:
- Custom ORM over a direct query library → Does this help me ship? No. Use the library.
- Plugin architecture before you have plugins → Does this help me ship? No. Skip it.
- Comprehensive logging and observability setup → Does this help me ship? No. Add console.log and fix it later.
- A landing page so users know the product exists → Does this help me ship? Yes. Do it now.
- The auth flow users need to log in → Does this help me ship? Yes. Build it this week.
The pattern is clear. Infrastructure that serves hypothetical future needs fails the test. Features that users will interact with pass it. When you are unsure, that uncertainty is your answer.
The Overengineering Spectrum
Not all overengineering is equal. There is a spectrum from "slightly unnecessary" to "completely detached from reality," and knowing where you fall helps you calibrate.
Mild overengineering: writing comprehensive unit tests for code that might change completely next week. Setting up a full CI/CD pipeline before you have a deployable product. Using TypeScript's most advanced type features when simpler types would work. This stuff is not catastrophic, but it does slow you down.
Moderate overengineering: building a custom component library instead of using an existing one. Writing a migration system instead of manually running SQL. Creating an abstraction layer over a third-party API "in case you want to switch providers later." This costs days to weeks and delays shipping noticeably.
Severe overengineering: implementing a microservices architecture for a single-user app. Building a plugin system before the core product works. Writing a custom framework instead of using an existing one. This costs weeks to months and is often the thing that kills the project entirely because you run out of motivation before the infrastructure is done, and you never get to the actual product.
I have been guilty of all three levels. The mild version is hard to avoid entirely and not worth stressing about. The moderate version deserves attention. The severe version should set off alarm bells. If you recognize yourself in the severe category, stop coding and go read about how to ship faster before you write another line.
Strategy 1: Build for 10 Users, Not 10,000
This is the single most effective mental shift for stopping overengineering. Instead of asking "how should I architect this to scale?" ask "how would I build this if I knew I would only ever have 10 users?"
With 10 users, you do not need a message queue. You do not need horizontal scaling. You do not need database sharding. You do not need a CDN. You do not need rate limiting, load balancing, or a microservices architecture. You need a single server, a single database, and code that works.
This is not a thought experiment. For your side project's first version, 10 users is a realistic ceiling. Most side projects never get past single-digit users in their first month. Building for 10,000 users when you will have 3 is not prudent engineering. It is a waste of time.
And here is the thing that makes this strategy safe: scaling up is a well-understood problem with known solutions. If your side project somehow goes viral and you need to handle 10,000 users, you can scale then. The knowledge exists. The tools exist. The problem is solvable when it is an actual problem. Right now, it is a hypothetical problem, and solving hypothetical problems is overengineering by definition.
Strategy 2: Skip the Abstraction Layer
I know this will make some developers uncomfortable, but hear me out: for your side project, skip the abstraction layer.
Do not build a repository pattern on top of your database. Just write queries. Do not build a service layer between your API routes and your business logic. Just put the logic in the route handler. Do not build a custom hook that wraps another hook that wraps a third-party library. Just use the library directly.
Every abstraction layer is a bet. You are betting that the thing beneath the abstraction will change, and when it does, the abstraction will make the change easier. For large, long-lived codebases maintained by teams, this bet often pays off. For side projects built by one person over a few weeks, this bet almost never pays off because:
You are probably not going to switch databases. You are probably not going to switch API frameworks. You are probably not going to refactor the thing the abstraction is abstracting over. And if you do, the abstraction you built probably will not match the actual change you need to make because you built it based on guesses about what would change, and you guessed wrong.
Write the simplest code that works. If it is messy, that is fine. If it has duplication, that is fine. If it violates some design pattern you read about, that is fine. You can refactor later, with the benefit of knowing what actually needed to change instead of guessing.
Strategy 3: Deploy the Ugly Version
There is a version of your project that could be deployed today. It is ugly. It has hardcoded values. It is missing error handling for edge cases that will affect 1 in 1,000 users. The loading states are wrong. The mobile layout breaks on small screens. The CSS has two unused classes that bother you.
Deploy it.
I am not being flippant. The ugly version that exists in production is infinitely more useful than the polished version that exists on your laptop. A real user interacting with your ugly product teaches you more in one day than a month of polishing in isolation. You learn what features actually matter. You learn what workflows people actually use. You learn that the mobile layout issue does not matter because 90% of your users are on desktop, and the error handling gap does not matter because the edge case never actually occurs.
The fear of deploying something ugly is rooted in ego, and I say that having felt it myself many times. You do not want people to see imperfect code. You do not want your name attached to something that is not polished. But that fear is the enemy of shipping, and shipping is the only thing that matters.
Set a deploy date. When that date arrives, deploy whatever you have. Then improve it based on reality instead of assumptions.
Strategy 4: Use Boring Technology
The new database you read about on Hacker News is interesting. You should not use it for your side project.
Choose boring technology. Postgres, SQLite, Express, Next.js, React — the stuff that has been around for years and has a boring, predictable interface — is a superpower for side projects. You already know how it works. The documentation is comprehensive. The Stack Overflow answers exist. The edge cases are documented. You will not spend two hours debugging a framework quirk because the framework has already had its quirks found and fixed.
Interesting technology is a form of overengineering because it introduces unknowns into your project. Every unknown is a time cost. "How does this work?" is a question that takes minutes to hours to answer, and with boring technology, you already know the answer.
I have lost entire weekends to debugging issues in bleeding-edge tools that would not have existed if I had used the boring alternative. The side project was supposed to be about the product, not about learning the nuances of a new framework's hydration model.
Save the interesting technology for learning projects where the point is to learn. For side projects where the point is to ship, use the most boring stack you can tolerate.
Strategy 5: Set a Hard Deadline and Work Backward
Nothing cures overengineering faster than a deadline. When you have four weeks to ship, you cannot afford to spend a week on the perfect abstraction layer. You have to be pragmatic because the math does not work otherwise.
Pick a ship date. Write it down. Make it public if you can. Then work backward from that date, allocating time to each feature. When you run the numbers and realize you do not have time for a custom event system AND the actual product, the event system gets cut. Not because it is a bad idea, but because it does not fit in the timeline. The deadline makes the decision for you.
This is why scope management and overengineering prevention go hand in hand. When your scope is locked and your deadline is fixed, the only variable is how you build each feature. And when time is limited, the answer to "how should I build this?" naturally shifts from "what is the best architecture?" to "what is the fastest thing that works?" The fastest thing that works is almost always the right answer for a side project.
FoundStep's Scope Locking helps here by fixing the scope side of the equation. When your features are locked and you cannot add new ones, you stop building infrastructure for features that do not exist yet. You focus on what is in scope, and you build it the simplest way possible because that is what gets you to launch.
The Permission to Be Simple
I want to say something that might sound obvious but that most developers need to hear: it is okay to write simple code.
It is okay to use a single file instead of a folder structure. It is okay to hardcode a value instead of making it configurable. It is okay to use an if/else instead of a strategy pattern. It is okay to copy-paste code instead of abstracting it. It is okay to use a simple array instead of a B-tree. It is okay to write code that would not pass a senior developer's code review.
You are not writing production code for a company with millions of users and a team of engineers who will maintain it for years. You are writing a side project that needs to ship before you lose interest. The quality bar is different because the context is different, and applying professional-grade engineering standards to a side project is a form of overengineering in itself.
Simple code ships. Complex code sits in repos marked "WIP." Given the choice, I will take shipped and simple over unshipped and elegant every time.
The best developers I know have learned to calibrate their engineering standards to the context. At work, they write clean, well-abstracted, thoroughly tested code. For side projects, they write whatever gets the product in front of users fastest. This is not inconsistency. It is wisdom.
If you are struggling to finish side projects, take a hard look at whether overengineering is the reason. In my experience, it is the reason far more often than people admit. The project did not get abandoned because the developer lost interest. It got abandoned because the developer spent so long building infrastructure that they never got to the product, and by the time the infrastructure was done, the excitement was gone.
The Best Architecture for a Side Project
I will tell you the best architecture for your side project. Here it is:
A monolith. One language. One database. One deployment. No message queues. No microservices. No separate frontend and backend repos. The most boring, straightforward, un-clever architecture you can build.
This architecture is best not because it is technically superior. It is best because it is fast to build, easy to understand, simple to deploy, and trivial to debug. When something breaks at 11pm and you have work in the morning, you want the simplest possible system to debug. That means a monolith.
You can always break it apart later. The path from monolith to microservices is well-documented. The path from half-finished microservices to shipped product is much harder.
Build simple. Ship fast. Refactor when you have real problems. Everything else is overengineering.
For the full solo developer workflow that pairs with this philosophy, that guide covers the day-to-day process of building with simplicity as a constraint.
FAQ
What is overengineering in the context of side projects?
Overengineering is building more complexity than the problem requires. In side projects, this typically means creating abstraction layers for code that will not change, optimizing for scale you do not have, building plugin systems for a product with zero users, or spending more time on architecture than on the features users will actually interact with. It feels like good engineering but it is actually procrastination.
Why do developers overengineer side projects?
Three main reasons: it feels productive because you are writing code and solving problems, it is technically interesting compared to the boring work of building basic CRUD features, and there is a genuine fear that doing it the simple way will create painful technical debt later. The third reason is almost always wrong for side projects because most side projects never reach the scale where the simple approach becomes a problem. You are optimizing for a future that probably will not arrive.
How do I know if I am overengineering?
Common signs include spending more time on architecture than features, building for thousands of users when you have zero, creating abstractions before you have a second use case, writing configuration systems instead of hardcoding values, and being unable to explain your architecture to a non-technical person without it sounding unnecessarily complex. If you have been working for a week and a user would not notice any difference, you are probably overengineering.
Is it ever appropriate to invest in architecture for a side project?
Yes, but only after you have shipped v1 and have real users. At that point, you have actual data about what parts of your system need to scale, what code actually changes frequently, and where technical debt is causing real pain versus theoretical pain. Architecture decisions made with real data are good engineering. Architecture decisions made before launch, based on hypothetical scenarios, are overengineering.
What is the fastest way to stop overengineering?
Set a hard deadline for shipping and work backward from it. When you have four weeks to ship, you cannot afford to spend a week on the perfect abstraction layer. Time pressure forces you to make pragmatic decisions, and pragmatic decisions are almost always the right decisions for a side project. Pair this with Scope Locking to prevent new features from sneaking in and demanding their own infrastructure.
Ready to ship your side project?
FoundStep helps indie developers validate ideas, lock scope, and actually finish what they start. Stop starting. Start finishing.
Get Started Free

