We all have apps running some “legacy code” in some “legacy way”. The term “legacy” means different things in different projects but we know when we see it and we want to get the time to modernize those apps in some way.
I recently went through the latest phase of modernization of a legacy app. Even though it’s a relatively small app, it thought me a number of lessons that’s worth sharing.
In this blog series, I want to outline the app and the modernization journey, the phases we went through, the decisions we had to make in each phase and talk about the final architecture in the end.
Hopefully, it will be interesting and relevant to a lot of people. Let’s get started!
Welcome to Amathus
Back in late 2015, a friend of mine and I were busy with our corporate software engineering jobs that left little choice with the tools, languages and frameworks we wanted to use. Both of our projects were filled with layers and layers of legacy and unnecessary complexity that sucked the joy out of software engineering.
We missed starting a greenfield project, working with the latest tools, languages and frameworks we wanted. Above all, we wanted to feel the accomplishment of finishing a project end-to-end, however small it might be.
Since we are both originally from Cyprus and avid news readers from back home, we decided to work on a news aggregator app. In this app, we’d collect feeds from newspapers from Cyprus and display them in a simple, clean timeline on a mobile app. My friend worked on the mobile app, while I worked on the backend. We called it Amathus, an ancient city in Cyprus :-)
We finished the prototype over a couple of weekends in late 2015 and restored our faith in our software engineering abilities :-) The frontend was written in Ionic for cross-platform mobile support. The backend was written in C# with ASP.NET framework. In this blog post series, we’ll focus on the backend.
I don’t know if you can call this an architecture but the initial backend looked like this:
It was written in C# on ASP.NET Framework (4.6), running on Windows. Deployed as a single app to a random machine provided by a random IIS host.
Even though it was deployed as a single app, in reality, it had 3 distinct parts:
- Reader: A scheduled service to grab RSS feeds from various sources.
- Transformer: A transformer to convert various RSS formats into a common format using the rules we defined.
- Web: A Web API to expose the transformed feeds for the frontend.
Pros and Cons
The initial prototype was primitive but had some advantages:
- Worked! Well, sort of. It wasn’t scalable or resilient but worked for our needs at the time.
- Easy to understand: A single app on a single machine. What’s not to understand?
- Easy to deploy: Pushing from Visual Studio to an ISS host was easy (unless things failed).
- Inexpensive: IIS hosting was quite inexpensive.
Even though the backend kind of worked at the time, we took a lot of shortcuts such as:
- Coupling: It was obvious from the beginning that this app was really 3 separate services. We tried to keep things de-coupled with separate libraries and it kind of worked but features did end up creeping in from one service to another.
- Bad DevEx: The ISS hosting was cheap but it provided zero developer experience. For example, I had to FTP into the machine in order to see the logs.
- No redundancy: It was literally a single app on a single machine. Sometimes the machine would go down and the mobile app would stop serving feeds.
- No persistence: Everything was in-memory. Sometimes the app would crash and it’d take some time to fetch all the feeds and fill memory again.
- No resilience: Because there was no redundancy or persistence, there was no resilience whatsoever. The reader would run out of memory and this would bring down the whole app with web API and all.
The initial architecture provided good lessons for the next phases:
- Stick to MVP: We initially thought we needed to read columns in addition to the news. As a result, we overcomplicated both reading and transforming feeds by trying to make it too general. We should have defined and stuck with the minimal viable product (news) and implemented that only. It was a pain to remove the unneeded column feature later.
- Research your options: Sure, the IIS hosting was inexpensive but the machine would go down frequently and we didn’t realize the DevEx was so bad, until we moved to the cloud (more on that later). Do your research before going for the cheapest option.
- Avoid coupling at all costs: Just because you deploy some functionality together does not mean that they have to coupled tightly in the code. Quite contrary, you have to make a concerned effort to define and keep different parts of the app (reader, converter, web) separate from the beginning with separate libraries, packages, tests. We didn’t pay attention to this initially and it turned out to be very difficult to separate things out later.
- Design with the future in mind: Even though you want to stick to MVP, you still want to design with the future in mind. You don’t have to overcomplicate the architecture with things you might not need but you have to allow flexibility in your architecture. For example, in prototype, we decided to keep everything in memory. It was the simplest thing to do and that’s all we needed at the time. However, we knew that at some point, we had to use some persistence. We failed to design the backend in a way that it’s pluggable. In later phases, we had to refactor code to make the backend pluggable to compensate for the initial inflexibility in the design.
The backend was far from ideal but it served us for about a year before we started looking into other options in late 2016 and early 2017. In the next post, I’ll talk about the lift & shift to the cloud phase of the app. Stay tuned!