Short answer: That’s simply not possible. We’re all humans so it’s hard to make something good on the first try. But it’s possible to minimize the number of mistakes and effectively fix everything later.
I've recently completed writing my latest program, and I must say, it's one of the most decent ones I've ever created. However, it wasn’t without its challenges and I hit the wall quite a few times. This experience taught me several valuable lessons, which I'll be sharing in this article. Here, you won’t find a bunch of cliche advice from ChatGPT but rather practical insights gained from my mistakes.
Thoroughly think over everything before writing any code
The program I was working on is a habit-tracking app. Not a simple one though. The matter is, many habit trackers and planner apps out there do not work as I’d expect. When I miss completing the habit on a due date, I expect the program to tell me about this backlog later. And vice versa, when I complete a habit on a non-due day, I want the next scheduled occurrence to be canceled. So I created my own that does exactly that.
It also calculates a habit streak. And after all calculations, it should look something like this:
Sounds simple in theory but in reality, you’re gonna hit some major pitfalls. To determine habit dueness, it’s not enough to check whether it’s due on the current date or not. If the habit has been over-completed previously, we want to cancel the current occurrence. And if the habit was failed on a certain date, we can’t surely say it’s failed because this backlog may have been sorted out later.
So how do we determine whether it’s actually due or not? By calculating the number of times it should’ve been completed and then comparing it with the actual number of completions? How do we calculate these numbers? By checking whether each date is due or not? Is it performant? How do we store all of these in a database? And remember that the results will be different depending on whether the given date is in the future or in the past. Finally, how do we then calculate streaks?
So, instead of thinking over all of those, I decided to write the code first. And, honestly, it didn’t turn out well. 🫠 I ended up creating an architecture where
- it wasn’t possible to compute habit status for a certain date without affecting other dates;
- I had to compute everything up to the current date;
- and the worst part, all this was stored persistently so I had to somehow update everything every time something changed.
I won’t go into the details of how awfully everything was implemented, but trust me, it was simply unmanageable.
That’s why we have to thoroughly think through every aspect of a project before touching the keyboard. While it might seem like a slower approach, I now understand that it leads to cleaner, more efficient code that is far easier to maintain and extend. Because it’s usually a lot more complicated than we think it is. Rushing to write the implementation will often lead to a huge amount of time spent for refactoring later. For this very reason, seniors spend a lot more time thinking than juniors.
Also, our thoughts do not extend to every single edge case. That’s where AI comes into play! It’s a good thing to ask AI about that, especially if you don’t have anyone else to discuss it with. I discovered that just recently when I asked about my next project’s implementation. And it dropped some really good points, which I’d otherwise not consider.
Adding features is easy, but maintaining them is hard
Some functionality may not be as useful as it seems. I had to drop some of my app's features because they were not worth the effort. I learned to avoid adding or expanding features on a whim or without a clear purpose. Be a lazy developer!
The thing is, even if some functionality seems easy to add, you have to remember that you will also have to maintain it, fix bugs, test and enhance it, integrate it with the rest of the app, optimize its performance, make sure it works on different devices, and so on. It will be frustrating if you eventually realize that you have to drop this functionality because it is not needed by the user or it actually hurts the user experience.
Follow git best practices
When I started working on my project, I didn't have a clear plan in mind. I was jumping from one task to another, resulting in a messy codebase because everything was done all at once. I ended up building on top of functionality that wasn’t working properly and wasn’t tested thoroughly. Debugging such a structure was also quite challenging. Not only could I not identify what wasn't working, but I also struggled to easily revert to the latest working state.
What I've learned is that it's crucial to have a clear goal for each part of the project and to focus on one task at a time. After completing a small chunk of work, test it thoroughly — either manually or automatically — to ensure it functions correctly. Only then should you move on to the next task.
This approach aligns perfectly with git best practices—making your commits small so that you can easily pinpoint the exact change that caused the issue. With this method, you’ll also be able to roll back to a previous commit without losing unrelated work.
I also discovered the power of pull requests. They are not only useful for team projects but also for personal ones. While you would typically want to keep your commits as small as possible, you can gather them in a pull request to form a feature. This way, you’ll have a clear separation of what you’re working on.
Whenever you start working on another feature, create a new branch and commit to it instead of committing directly to the main branch. And here’s the important part: you have to be confident in your code each time you hit the merge button. This ensures you don’t build further on top of broken code.
Adopting this workflow has other benefits. For example, you can leverage AI code reviews by setting up a GitHub action that will automatically review your pull requests. This way, you can discover things you might otherwise be unaware of because, after all, we don’t know what we don’t know.
This approach will also make it less tempting to make changes to unrelated code and instead focus on building the planned functionality. Follow the single responsibility principle!
Additionally, I suggest documenting your changes by writing descriptive commit messages. You’ll thank yourself in the future because future you won’t necessarily remember those details. You can also provide more context in the pull request details than you can in commit descriptions including images and videos.
What is actually an MVP
Finally, I made it work. And now Routine Tracker’s code is clean and works properly. But here comes the last insight from building this program.
When it was time to write the readme, I realized one important thing. I actually haven’t achieved the functionality I wanted to make initially. Apart from other habit-tracking apps, my app was supposed to display the habit’s progress and the estimated completion date. You see, I thought that I would first create a minimum viable product and then fill it with cool features.
However, what I discovered too late was that an MVP doesn’t presume a project with minimum features. It presumes a project with minimum functionality that is required to test whether people find your idea useful. So what kind of feedback can users give you if you provide an app that copies features from already existing projects and the only thing that differentiates your project is that it is immature and buggy? Instead, the MVP should contain those very features that set your app apart. In my case what I should’ve built first is habit’s progress and completion date estimation functionality.
How do you build those cool features if the foundation isn’t ready yet? The answer is in this tweet:
I was so focused on making that core functionality work that I forgot about the very purpose of the app. 😅 That was fine though because its core functionality is also kinda non-trivial.
Also, while pet projects are a playground for skill development, applying such business principles to them brings a whole new set of benefits. From faster development and preparation to a real-world scenario to these projects bringing more chances to give you a job.
Write the Readme first
You may have noticed that I gained the last insight while writing the readme. Why? Because the readme is where we have to outline the mission of our project, how it achieves it, and what makes it unique. So I had an opportunity to better think about that in the final stage of development. Cool, but it’s not very smart. What if we turn things around and write the readme on the planning stage of the project before writing any code? This is called Readme Driven Development and it’s not a new idea.
This also makes it possible to receive some initial feedback by sending this readme to other people and asking what they think.
👋 Hi, I’m Daniel
I’m a native Android developer. I’m currently focused on building a strong portfolio, improving my skills, and establishing an online presence. If you want to collaborate with me in any way, please let me know.