Understanding the Pitfalls of Monolithic Architecture
What happens when we ignore software architecture over the long term? The result looks a little like this:
How do we end up with this mess? Traditionally, applications are built for a single purpose. Think of a calculator application. It’s pretty simple. Take in a bunch of numbers, an operation or two, do the math, and then output the result. We can slam that out, no problem.
Since it only needs to do that one thing, we build a monolithic application. The application does everything related to that one thing: code for data access, business logic, and the graphical interface are all bundled up in a single inter-connected layer.
Why is that bad?
Think of a layer cake. There is frosting and cake, and more frosting and more cake. Each section contributes to the goodness. On the other hand, we could just put all of it in a blender and mix up all the flavors. The layers are gone, and we can’t tell what’s frosting or what’s cake. 🤢 It will take a large glass of milk to remedy.
Now, let’s go back to our calculator application example. What if our customers need us to integrate new functionalities? Let's say we want to provide a REST API (application interface that can be called from anywhere). We want to save the values input, the operations asked for, and the results so that we can perform some analytics on that information later (or in real-time). We want to add new operations to support some scientific or business calculations, which means more buttons on the interface.
We could add on the new parts in an ad-hoc fashion. But then we end up with a monolithic application that is difficult to modify (picture those plugs in the image above). Logic is buried deep in code somewhere, which means it's difficult to predict what might break when making a change.
While you may know better than to code this way, there are a lot of legacy systems out there that have evolved into this state. It happens. No one sets out to create a nightmare. But that’s kind of the problem few developers take time to fix as they go.
How do we fix an application like that?
That’s exactly what I’ll show you how to do in this course! We’re going to take a monolithic application, analyze the issues, refactor it into separate components, and facilitate communication between different layers. Now, let’s dive into the specific example that we’ll work through together.
Case Study: Fix a Hard-to-Modify, Non-Scalable App
Dairy Air, a small charter airline, has asked you to help them with their application. Their current one supports the scheduling of specialty flights and runs on a desktop at the airport where the company is based. One person opens the application and does all the scheduling necessary. So far, everything is fine. It was what the airline needed when the company got off the ground (ahem). However, it’s no longer serving the clients’ changing needs.
Specifically, the airline has a new potential customer. A tour group would like to use them regularly to fly people to exciting locations. Currently, they’ve just flown individuals or a few people to business meetings. This new opportunity represents an entirely new line of business. And an exciting one for growth!
Unfortunately, the current application suffers from several aspects that do not scale. That is, as more demands are put on the system, the system will perform slowly or not at all. Let’s look at a couple of these limitations:
It is currently only accessible by one person. It will need to be available to company representatives at other airports. Plus, the tour group booking agents will need access. Eventually, it would be great if individuals could book charter flights as well.
The database does not support concurrent connectivity for querying and updating.
The business logic for finding available aircraft and pilot combinations is buried in code that isn’t easy to find, never mind modify.
Now, let's look at the nuts and bolts. The application is written in Java. The only classes are:
Client (Represents who purchased the charter trip)
Pilot (Someone who is qualified to fly an airplane)
Plane (Vehicle that takes passengers from one location to another)
Reservation (Details about a trip)
The current architecture also consists of a handful of Java Swing classes for rendering and SQLite database for storing information about the pilots, planes, reservations, and clients. Event handling and business logic is buried in the Java.
As you can see, there are a lot of connections between classes. Particularly everything connected to the Java Swing and SQLite areas. What an excellent opportunity to incorporate a decoupled web architecture!
Great...but what is that?
Decoupled means that the various parts don’t know a lot of details of the other parts of a system. They communicate through well-defined interfaces. For example, when you drive a car, you turn the steering wheel (an interface). It is somehow connected to the wheels. You don’t need to know all the details about bearings and linkages to make the car turn.
What advantages does decoupling yield?
A decoupled architecture allows all components to operate independently. Modifying the implementation of one does not impact any of the others that depend on it. In other words, the execution of the code in a layer can change without affecting the other layers!
So...how do we fix it?
To architect a better solution (a decoupled one), we can apply the model-view-controller (MVC) pattern. This pattern will separate responsibilities in the system into distinct layers and items.
What do the layers do?
That's what this course is about! But a quick introduction:
User interface: Interacts with the user, displays information and collects input.
Entity layer: The items of interest that are being displayed, or manipulated by the user interface.
Data layer: If the entity items need to be remembered, this will store and retrieve them from a long-term data storage solution.
Connecting layer: Glues all the layers together, so they don't need to interact with one another directly.
Each layer will communicate to the connecting layer through clearly specified interfaces. In this way, we can replace the functionality behind the interface with different implementations, and not affect the caller of the interface. (Interface refers to the messages and functions provided by each layer, not explicitly the user interface, which is a layer providing an interface.)
To figure out what we need to change and how to do it, we'll go through the following steps:
Express business opportunities as user stories.
Expand the user stories into use case descriptions & figure out what entities need to be added to our application.
Ask ourselves key questions to determine what part of the current application is weak and what can be salvaged.
Prioritize our user stories based on our findings.
By analyzing our needs, planning out our changes, and prioritizing them, we maximize our time and energy - and keep from getting lost in a sea of changing code!
Once we have this done, we will apply an MVC pattern in a bit-by-bit approach using refactoring. Refactoring allows us to make incremental changes, while still having confidence in the system we are creating.
Let’s go and build a better application for our customer!
Monolithic applications are difficult to modify and scale.
We decouple our architecture through applying MVC and refactoring.
Now that you know what to fix, let's take the first step in fixing it: defining our user stories!