How I Reevaluated My Tech Stack

In 2021, when a financial management app from the AppStore lost over 2 years of our data, I built an MVP of econumo in just 2 weeks using PHP + Symfony + PostgreSQL + VueJS. However, after 3 years of support, I reevaluated the tech stack for the backend, and here’s why.

Let’s Dive In

I like modern PHP. It has significantly evolved over the past few years: developers have enhanced both the language itself and its performance. PHP has developed an infrastructure, and in the development community, Laravel is increasingly being cited as a prime example of a simple framework with a low entry barrier and a wealth of possibilities. Rector has appeared, simplifying migration not only for popular libraries but also for language versions (Rector automatically updates your code, applying features from new versions and replacing deprecated elements). While the downsides remain, if you acknowledge them and know how to handle them, they’re not particularly problematic.

Since 2014, I’ve been using Symfony. It’s a serious framework that offers an excellent out-of-the-box foundation for application development, following best practices (often borrowed from Java), and consequently having a steeper learning curve compared to Laravel, for example. It’s a great framework for team projects, but I must admit, we often spent more time on configuration than on coding.

PostgreSQL is a powerful, free, and open-source DBMS with flexible scaling capabilities and various configurations for different cases, from high loads to big data. It’s the default choice for many developers across different PLs.

You may argue that these tools come with a multitude of problems and complexities (and you’d be right), but that’s not the point here.

In the development world, two main opinions circulate:

  • For a quick start, begin with what you know well;
  • For each task, use the tool that suits it best.

In 2021, I thought my stack met both points, but three years later, I can tell you where I was mistaken (and it’s not about performance).

Infrastructure and Its Operation

I understand that I’m not a DevOps, so I prefer to keep my infrastructure as simple as possible—fewer potential problems, plus it’s easy to write Ansible scripts. Scaling and other high-load joys are unnecessary for personal projects, but having backups and Infrastructure as Code is a must.

If you don’t install PHP on the system but use Docker-compose, projects with low traffic certainly won’t have issues. The consistency of environments is convenient for development. With Rector, updating versions doesn’t seem difficult, but you still have to spend considerable time reviewing changes and performing regression testing.

PostgreSQL can also be used in Docker-compose, but you’ll have to tinker with setting up backups and the update process (especially major versions). This goes unnoticed when working in a team with designated roles, but when you tackle this alone in your free time (it’s a pet project, after all), you realize it’s quite resource-intensive.

To relieve some of the load, I decided to try DigitalOcean’s AppPlatform. I simply provided a Docker image with the application, ordered Managed PostgreSQL, and happily started paying $50 a month for this beauty. And all on the minimum possible capacity.

Package Support and Updating

Symfony is actively evolving and has successfully released 2 new major framework versions in 3 years. I initially settled on an LTS version with the intention of not updating frequently, however, my deprecation.log already doesn’t look good. I couldn’t use Rector to update package versions due to different dependencies.

At some point, Dependabot brought me a critical update for one of the core packages, which dragged along updates for other packages, eventually breaking my authorization. Changes had to be rolled back, and the update was temporarily abandoned.

Not Obvious at First Glance

Returning to the realization that I had chosen the wrong stack for the project, I would like to elaborate a bit on this thought.

Thanks to the principle

For a quick start, begin with what you know well

I started quickly, and we began using econumo within a couple of weeks, which gave me motivation to continue working on the project, improving and adapting it to our needs.

But when I thought about

Use the tool that fits the task

I evaluated the project from a technical perspective. It’s a personal project, no load, potentially open source. A relational DBMS is a perfect fit. PHP won’t cause issues. If necessary, horizontal scaling is possible.

However, in the long run, the vector of evaluation turned out to be incomplete, and I got bogged down with infrastructure and packages, increasing the risk of demotivation and stopping the project’s development and support. On top of that, after a year of hosting, I realized I had spent $600 purely on infrastructure, which isn’t much for a hobby but, overall, could have been spent on something else.

Solopreneurship

I nearly came to terms with being stuck on older versions for the moment. Then I bought a $5 VPS from Hetzner and deployed the entire infrastructure in Docker, reducing regular expenses tenfold. By the way, nothing happened over the year, and everything runs smoothly.

But the thought that something was wrong kept coming back. Then I stumbled upon an intriguing video - https://youtu.be/lASLZ9TgXyc?si=S8iUgjFWxhiC-gma (it’s not about htmx) and it dawned on me how things could be done differently.

It became clear that if it had been a commercial project and a team was working on it, there wouldn’t be any problems. However, the main constraint, which wasn’t apparent initially, was me. Or more specifically, the fact that I was working on the project alone, with irregular hours that I could dedicate to development. I realized I could invest minimal resources in supporting infrastructure and updating dependencies if I adjusted the stack.

Replacement

I decided to replace PostgreSQL with SQLite. The initial reaction from many developers was, “are you stupid?”. But if you set aside familiar arguments, you’re left with a set of dry facts:

  • SQLite is a library, very stable. SQLite3 has been around for about 20 years. No need to migrate the database; updates come with the operating system. Backward compatibility is 100%.
  • Making backups is very simple; no additional tools are needed, especially while the database is small.
  • Launching new environments, running tests in CI, and debugging issues is super easy - just copy/create the database file.
  • For econumo intensive writing isn’t required, but a lot of reading is needed. And in this, SQLite excels!

In general, I exported SQL from PostgreSQL and created an SQLite database based on it – not a single problem! During application adaptation, I faced a few issues (more on this below), but overall, the migration from one DBMS to another was quite successful. And as a bonus, compared to the average response time in the DO cloud for $50 a month at 400ms, the response time for a $5 VPS with SQLite dropped to 50ms.

Realization

While migrating, I encountered some peculiarities of SQLite adapter implementation for Doctrine (for example, working with date fields in SQLite), due to which I switched from using ORM methods to regular SQL queries in repositories. This triggered me to the following thoughts:

  1. If I had simply used PDO with regular SQL queries, a simple project configuration change would have sufficed.
  2. The belief that Doctrine provides a universal interface and allows easy migration from one DBMS to another is erroneous. Doctrine provides a universal interface, which complicates migration because you don’t know if all necessary abstractions and functions are implemented.
  3. Doctrine provided several great tools: repositories, data mapping to entities, fixtures, and migrations. However, they can easily be implemented oneself in a minimally necessary form.
  4. I caught myself realizing that I forgot the SQL syntax for creating/updating tables, indexes, foreign keys, since I was used to generating them through Doctrine. But when I started generating migrations for SQLite, I found a couple of implicit bugs I wouldn’t have noticed had I not dealt with foreign key operations in SQLite.

These doctrinal thoughts led me to understand that for a solo developer, all this abstraction, initially perceived as valuable, is actually a problem. If you’re an experienced developer, you don’t need all these academically correct libraries and abstractions. You can design an application that performs its function without a framework. When an application is large and a team is working on it, ready-made architecture and frameworks help the team synchronize, simplify onboarding of junior developers, and transfer knowledge and code. But when you’re alone, you don’t need this. I realized that I don’t need all these external libraries and dependencies. I’m perfectly capable of implementing a very simple Dependency Injection, controllers, repositories, authorization, etc. myself. And it will be much simpler and more flexible, as I’m solving my specific tasks. Updating such a project will be very simple and fast.

I’m not going to rewrite the project now, but I’ve drawn conclusions for the future.

Go-go-go

After some more reflection, I wondered what technology would make project maintenance as simple as possible, comparable to SQLite? In the YouTube video above, the speaker uses Node, which isn’t much different from PHP, but the idea is that Node is a simple and stable framework that rarely changes.

I decided to abstract and started running through my head programming languages that are:

  • Compilable (the project won’t build as long as there are errors);
  • Cross-platform (at least linux/amd64, linux/arm64, or mac os);
  • With a good standard library that doesn’t change often (no need to drag a lot of dependencies);
  • Simple. And I realized, it’s Go!

In addition to the list above, it became clear that Docker isn’t even needed for it, and the service runs perfectly through systemctl. Forget about performance issues altogether. This means you can host multiple applications on a $5 VPS!

Conclusions

After three years of working on my pet project, I reconsidered the chosen tech stack (for personal projects) in favor of:

  • CloudFlare - DNS hosting + issuance and update of SSL certificates (free).
  • VPS on any hosting (I used DigitalOcean, Hetzner, and am currently on the free OracleCloud).
  • NGINX - as a reverse proxy. Easy to configure, ensures secure connection between Cloudflare and server.
  • GO - as the development language. A compiled, cross-platform, simple language with a good standard library.
  • SQLite - the simplest DBMS that doesn’t need updating and is easy to back up. In case of anything, it’s easy to migrate data to another DBMS, or use Turso.

Additionally, I included in the project evaluation the factor of WHO, WHEN, and HOW LONG the maintenance would be carried out. And now I’m contemplating whether all these layers are needed in commercial development, but that’s a separate question.


When it comes to the frontend, things are a bit more complicated, but there are some recommendations circulating to use HTMX and Alpine.js. I haven’t tried them myself yet, but once I figure it out, I’ll write a separate post.