← BLOGHOME↗

Mar 10, 2026 · 7 min read · Career Growth and Technical Leadership

Five Years Maintaining Enterprise Software

Most of what I learned came from changing systems that already had users, history and dependencies. I also stayed long enough to maintain some of my own earlier decisions.

I spent more than five years at Softcraft working on enterprise and government systems. During that time, I built new features, optimized slow workloads, modernized legacy code, improved deployment processes, supported production, reviewed code, and participated in architecture decisions. The variety was useful, but staying with these systems for years taught me more. Five years is long enough to see some of your own decisions come back as maintenance work. Not every awkward part of the code was inherited. Some of it was mine, and that changed how I think about engineering.

The Shortcuts We Maintained

A shortcut isn’t automatically wrong. Products have deadlines, teams have limited capacity, and sometimes the responsible decision is to deliver a smaller solution with known limitations. The problem is when everybody forgets the limitation, including the person who introduced it. A temporary condition becomes a permanent branch. A duplicated rule evolves differently in two places. An integration built for one consumer becomes an unofficial contract for several others. The cost usually doesn’t appear in one big failure. It adds a little more work to every future change. I became more deliberate about writing these trade-offs down. If we chose the faster path, we recorded what it didn’t solve and what would make us revisit it. Memory wasn’t a reliable maintenance strategy. Legacy code can be frustrating. I’ve definitely opened an old method and wondered why anybody would write it that way. More than once, the answer was that it contained years of business knowledge that the method name didn’t reveal.

Before changing an unusual rule, I learned to ask who depended on it and why it existed. Sometimes the behavior was accidental. Sometimes it protected an edge case discovered through a difficult production experience. Understanding first didn’t mean preserving every old decision. It meant modernizing with evidence instead of assuming newer code would automatically be more correct. This was especially important in systems that couldn’t simply be taken offline while we rebuilt them.

I maintained a few areas long enough to see a solution I had considered simple become difficult after the requirements changed. One duplicated rule was harmless while both workflows used the same conditions. Later, one workflow received an exception and the other didn’t, and we discovered that the duplication had never been made visible as a trade. I couldn’t have predicted every future requirement, but I could have left the next engineer a clue that two versions of the same decision existed.

On another feature, we introduced an abstraction before the two workflows were stable. It reduced code in the first release but created several hooks for differences that kept growing. I had been trying to avoid duplication and ended up making each business flow harder to read. Years later, I’m less uncomfortable with a small amount of repeated code when the concepts are still changing independently. I can extract what is actually shared after the pattern becomes clearer.

Changes Across Teams

Enterprise systems outlive team structures. The person who designed a module may move to another project, and the team that consumes an integration may change while the contract remains. I saw features become difficult not because the code had stopped working, but because nobody was sure who owned the decision anymore. Documentation helped, but ownership also needed to appear in the way modules, services and alerts were organized.

When we separated parts of a monolith, the repository boundary was the easiest part to see. The harder questions were who owned the data, who received the production alert and who could approve a contract change. If those answers remained shared between several teams, the service wasn’t as independent as the architecture diagram suggested. Some extra coordination was unavoidable, but making it explicit was better than discovering it during a release.

Code reviews became one of the places where context moved between people. An engineer working on the new behavior could explain the requirement, while somebody who had handled an older incident could point out the failure path. I stopped treating those conversations as delays around the implementation. They were often how the system’s history reached the current change.

Production and Deployment

A design that looks clean in code can still be difficult to deploy, observe, or recover. Production support made me check how we would notice a delayed workflow, whether an operation could be repeated safely, what happened to work already running during a deployment and whether the team could respond without the original developer. CI/CD, containerization, observability, and infrastructure became part of my software engineering work because they shaped whether the application could be changed safely. One deployment automation effort reduced the manual work by roughly 70%. What I noticed day to day, though, was that releases became less dependent on somebody remembering a fragile sequence of steps. One of the platforms supported thousands of daily operations. When critical workflows slowed down, it was tempting to blame the framework or ask for more infrastructure. By tracing API and database behavior, we found focused changes that improved response times by more than 60% in important workloads. Since then, I’ve spent more time understanding the current system before proposing a larger one.

The deployment work also changed how I divided application changes. A database migration, backend release and consumer update were easier to recover when they were compatible steps instead of one large switch. This sometimes meant keeping an old field or path for longer than I wanted, but it gave us a way to observe the new behavior before removing the previous one. The temporary state wasn’t beautiful. It was still better than a release where rollback required undoing data changes under pressure.

Production incidents showed me how much of the system existed outside the source code. A retry policy could increase traffic during an outage. A health check could stay green while a complete workflow was delayed. A log could contain a correct error and still be useless because it had no operation identifier. After investigating these cases, I thought about the failure and recovery path earlier in development, not because every feature needed the same level of resilience but because I knew which questions would appear later.

The same principle applied beyond performance. Clear diagnosis prevented unnecessary rewrites, services, and abstractions.

The Technology Changed

During those years I worked with Java and Spring Boot, Node.js and NestJS, PostgreSQL, Redis, containers, CI/CD platforms and cloud services. Learning the tools mattered and some of them made a large difference in how we built the systems. I also watched teams reproduce the same design problem with a newer framework. A service could be too coupled in Java or TypeScript. A queue could improve recovery or hide failed work. A new database could match an access pattern or become another system nobody knew how to operate.

I became less interested in choosing technology from a general ranking and more interested in what the current team needed to build and support. Java was a good fit for several long-lived transactional systems. Node.js was productive for integration-heavy services and APIs. PostgreSQL handled more workloads than we first assumed when the access patterns were understood. None of these choices removed the need to define ownership and failure behavior.

Staying with the systems after the first release also made technology costs visible. Dependencies needed updates, pipelines needed maintenance and the person who introduced a tool wasn’t always the person handling its next incident. I still like trying new technology, but I want a production reason for adding it to a critical platform rather than a cleaner first implementation alone.

Work That Didn’t Produce Much Code

In the later years, some days with very little code were still useful. A requirement became clearer before implementation, a review found a failure case or a pipeline change removed work from every future deployment. I still wrote plenty of code, but I stopped using the number of lines as a measure for the day. After more than five years, the systems weren’t finished or perfectly clean. They had more users, rules and integrations than when I started, and some old decisions were mine. I mostly wanted the team to keep enough understanding of them to make the next change without being afraid of everything around it.

I don’t think five years gave me a final set of rules. Some preferences became stronger and others became much less absolute. I care more about clear boundaries, operation identity, production evidence and changes the team can reverse. I’m also quicker to admit when the domain or the existing system knows something I don’t yet.

I saw these projects at the beginning, during planned modernizations and through the normal years in between, when requirements kept arriving and we had to work with earlier decisions. Some of those decisions were mine. More than once, I opened code I had written years before and needed time to understand why I had considered it a good solution.