The Reality of Long-term Software Maintenance
Written from the perspective of Dávid, our Software Architect:
This year, I started a deep review of the systems I lead, maintain, or supervise. This is only possible because I always push clients to allocate either a regular maintenance budget or a one-time budget for reviews. Even when an application is stable and no new features are planned for months or years, someone needs to keep an eye on it.
The reality of software is that what we plan years in advance rarely matches what we actually need today. User bases shift over time, and business plans get adjusted, which means features we spent weeks designing complex architectures for might end up being cut from the roadmap entirely. When those features are discarded, we are often left with a highly complex infrastructure that serves no real purpose other than consuming hosting budget and making simple updates tedious.
Dealing with past architectural choices
I have to be honest here: I have made some of my past architectural choices that don’t fit today’s needs. To me, a good architecture is not one that is pristine or flawlessly planned on day one. It is simply an architecture that allows you to make major changes later with the least amount of friction. Right now, my main focus is reducing service counts, converting some projects back to monoliths, and simplifying the flow of asynchronous messages.
Microservices
During my recent reviews, the most common changes I have been making have been to microservices. A few years ago, they were the default recommendation for almost every new project, so it is not surprising that they ended up in systems where they do not actually add any real value.

Event-driven architecture
I see a very similar pattern in event-driven systems where microservices are theoretically supposed to shine. The issue here is often an excess of asynchronous events with far too many hops between different services.
I recently worked on a project where a simple chat message had to travel through four different services, generating three separate AMQP messages along the way, just to reach the recipient. Onboarding a new team member and trying to explain that entire journey was incredibly difficult because there was no single, logical place to follow the flow.

By changing how those services were wired together, we managed to get the message to its destination faster, and we made the entire system much simpler for the team to understand and maintain. The new setup might not look academically perfect on a whiteboard, but sacrificing that theoretical design was absolutely worth the massive improvement in daily developer experience.
Deconstructing a seven-year-old system
To give you a practical example, I have been maintaining an internal system for a client for seven years now. It has a stable user base of about 200 people. Originally, the architecture consisted of two frontends (an admin back office and an employee app) and a backend with two integration API layers serving each frontend. On top of that, we had seven other microservices and two databases, specifically MongoDB and Redis.
On paper, everything was decoupled and highly organized. The permissions were clean because each API layer only served one frontend, and Redis handled our session storage. Over time, however, the hosting costs for so many separate services started to bother me, and doing basic dependency updates across so many different repositories felt like repeated, unnecessary work.
After reviewing the actual traffic, analyzing our data, and discussing future goals with the client, I decided to remove the two API integration layers and replace the Redis-based session storage with a stateless session approach. That change alone was a significant improvement. Initially, I did not plan to go any further, but because it went so well and took only a little over a day, I was excited to push the simplification further, especially since we had the budget allocated.
From eleven services down to three in one week
Because the original backend microservices were built in Node.js with Feathers.js, merging them was quite straightforward. The framework is service-based, which made it easy to take those separate components and plug them directly into a new, modular monolithic core.
I did not merge absolutely everything. There are always some services that genuinely benefit from running independently, so I left our notification service untouched. But as a result of this cleanup, we went from running a bunch of backend services down to just three, including the database.

The important thing to note here is that, because of how the services were originally structured, this did not become a month-long migration. The entire process took us only five working days.
Why the client actually saved money
Shortly after we finished the cleanup, the client came back with a list of new feature requests after years of frozen development. Because we had just simplified the architecture, our estimates for this new work were significantly smaller than they would have been just a week prior. This means the client will get their investment back very quickly through both lower development costs and reduced hosting bills.
If you work with clients who are hesitant to pay for this kind of preventative maintenance, I usually suggest setting up a small, regular maintenance package. Even if the monthly budget is modest, it accumulates over time and eventually provides the resources you need to simplify the system. As a bonus, your team gets a much better developer experience, a benefit that is often undervalued.
What I have learned about architecture over time
Time has a way of revealing which parts of an architecture were built for real needs and which were built for an imagined future scale. Many of the microservices I see in production today exist simply because they were the industry standard when the project started, rather than because they solved a genuine problem with scaling, independent deployments, or team structure.
I am not saying microservices are always a bad choice, and I still deploy them today when the situation calls for it. Some services really do benefit from running entirely on their own, much like the notification service we decided to keep separate. But in smaller or highly stable systems, they often just add unnecessary overhead. Dealing with separate hosting configurations, debugging issues across multiple repositories, and managing multiple deployment pipelines makes everyday work feel much heavier than it needs to be.
The systems that age the best are almost always the ones that stay only as complex as the current problem requires. When those business requirements eventually shift, the architecture should be flexible enough to adapt without requiring a heroic effort from the engineering team.
If you are currently maintaining a system that feels overly complicated, it might be worth taking a step back. Have an honest conversation with the people who actually use the application and look at the actual traffic in production. Once you have that data, you can make a pragmatic decision on whether the current setup still serves the business or if it is finally time to simplify.
The value of staying around for the long haul
I have always preferred long-term partnerships with clients. When you stick around a project for years, you get to take advantage of deep historical knowledge. You remember why specific decisions were made, see how those decisions play out in the real world, and actually get the chance to clean up your own codebase as the business needs change.



