
Marcos Henrique
Dev com negócios
How I Made My Software Faster Using These Techniques (Part 1)
One of the biggest learning jumps I had was when I launched my first project in production. It’s something I strongly recommend for any dev—the sooner, the better. Stepping out of your comfort zone and dealing with the classic "it works on my machine" brings a lot of learning.
One of the main lessons I learned was about performance and scalability. On our machines, tests usually run with 10–50 records, 3–4 registered users, and the same expected sequence of clicks to verify everything is working.
In the real world, that changes completely: multiple users using the service at the same time, records being added every minute, and the app starts getting slower and consuming more resources (💸). That’s when the famous question pops up: “Huh? It works on my machine.” That’s the pain of growth—your software needs to be more optimized as it scales to keep delivering the same experience.
And that’s what we’ll talk about in today’s post!
The first version of COSTPAT (a fixed asset management system I developed) was released around January 2022. It was a simple version but met the main needs of the company partners—who, by the way, are now my business partners. From that point on, my performance problems began.
After migrating the first client’s data (which was done the day before the launch), they noticed slow loading times for items (there were over 1,000 assets registered). That’s when I had my first “huh?” moment—everything was fine on my machine, but then I remembered my test database only had 100 items. So the next day, I implemented the first improvement:
1 - Pagination
I always wondered why big sites like Americanas (where I loved browsing toys as a kid), Magazine Luiza, etc., always had a footer with a huge list of pages that forced me to scroll to the end to see more products. And it was when I faced this performance issue that I remembered the likely reason for it.
Still looks like this today
Imagine this: you have an electronics store website like Kabum. At first, it only has about 50 products. So, no need for pagination—the user experience is better seeing everything at once. But now your store has grown to 500 products: graphics cards, CPUs, peripherals… Is the user really going to scroll through all 500 items? Probably not. Most users will search for something and check the top 15 results.
The issue with loading all 500 items is that your app needs to fetch them from the database and render them all, causing slow initial load times—both for the backend and frontend. That results in a poor experience (lags, delays) and worst of all, lost sales.
This is where pagination helps. Instead of returning 500 products at once, we divide them—for example, into 10 pages of 50 items—greatly reducing the app’s load time.
This small change solved the issue in my app, turning multi-minute requests into millisecond responses.
The more items you have in the database, the more noticeable the difference. From personal experience: in 2023, we added a client with over 100,000 items. I removed pagination for testing and... the result was extreme: database limits were hit, and the browser froze trying to handle so many items. In short: as data volume grows, pagination becomes essential.
2 - Data Selection
Even after adding working pagination, I wasn’t fully satisfied. The response time with pagination still wasn’t good enough. I wanted to go further, especially since most of our users were public servants. So I started looking more closely at what was actually coming from the API.
I realized all 41 columns from the database were being returned, even though only 6 were used on that screen. In other words, I was sending way more data than needed, increasing response times and putting pressure on users' network connections.
Columns used in the new version of the software
So I began selecting only the columns actually needed. This made a huge impact on performance, reducing response time and loading data much faster.
As Baloo from The Jungle Book would say: “Just the bare necessities!”
Time went by, new clients came in, and I, as always, had a tab open with the DigitalOcean console (our cloud provider to this day). One of the most used parts of COSTPAT is inventory—when city hall or council employees access the system to register assets and flag irregularities. This is when the system sees the most simultaneous users (I’ll probably write a separate post on concurrency handling).
During this time, CPU usage would reach up to 80%, along with the database—expected, since we were using the cheapest $5 machine at the time.
However, I noticed many of those queries were repetitive. Some items were accessed over 10 times, and the data didn’t change. So I thought: how can I optimize this?
3 - Caching Information
Something I see in many courses (wrongly) is the idea that caching should be used for everything and will solve all performance issues. But in the real world, every technical choice has both benefits and side effects.
The main benefit of caching is reducing the number of resource requests—whether from backend to database or frontend to backend. In COSTPAT’s case, frontend caching wouldn’t help much, since different users accessed the same item. So each one opening item 02 would still generate two separate requests.
The solution was backend caching. I cached not only the most accessed items but also the first page of assets and products, making the first-time user experience much faster.
Sounds simple to implement, right? Just add Redis (the in-memory key-value store I picked for its speed and easy setup on our server) and store items with a 5-minute expiration. But the real problem is cache invalidation.
With a cache in place, item 01 would be fetched from Redis. But imagine this: within those 5 minutes, user 03 updates item 01. What happens? Other users keep seeing stale data from Redis until the cache expires.
To fix this, you need cache invalidation logic: when an item is updated, delete its key from Redis. The next request will then fetch from the database and refresh the cache.
In my view, the downsides of overusing cache include:
- Some data can’t be stale—like a bank balance.
- In some cases, the invalidation logic gets too complex—if the item changes more than it’s read, the cache isn’t useful and needs invalidation all over the code.
So always evaluate whether caching is actually necessary—especially on the backend, which affects all users (e.g. a city hall). On the frontend, it only affects the specific user, and techniques like "stale-while-revalidate" are common (maybe we’ll explore that in Part 2).
In COSTPAT’s case, it was absolutely worth it—saving a lot of DB usage and, most importantly, money 🤑.
These were the 3 main improvements I made in the first version of COSTPAT, and they saved me a lot—technically and financially. In 2024, I decided to rebuild COSTPAT from scratch to make it faster and easier to use. No regrets—it was months of work that led to amazing results.
If you’re curious about the other 5 techniques I used in this second version—which allowed the system to double its users and data volume without increasing costs—drop a comment below 🙂.
That’s it for today—see you in the next one!