Docker and Kubernetes for Java Developers
上QQ阅读APP看书,第一时间看更新

The monolithic architecture

In the past, we used to create applications as complete, massive, and uniform pieces of code. Let's take a web MVC application for example. A simplified architecture of such an application is presented in the following diagram:

As you can see, the diagram presents the typical web application, a fragment of a banking system in this case. It's the Model View Controller (MVC) application, consisting of models, views, and controllers to serve up HTML content back to the client's browser. It could probably also accept and send the JSON content via the REST endpoints. This kind of an application is built as a single unit. As you can see, we have a couple of layers here. Enterprise Applications are built in three parts usually: a client-side user interface (consisting of HTML pages and JavaScript running in a browser), a server-side part handling the HTTP requests (probably constructed using some spring-like controllers), then we have a service layer, which could probably be implemented using EJBs or Spring services. The service layer executes the domain specific business logic, and retrieves/updates data in the database, eventually. This is a very typical web application which every one of us has probably created once in a while. The whole application is a monolith, a single logical executable. To make any changes to the system, we must build and deploy an updated version of the whole server-side application; this kind of application is packaged into single WAR or EAR archive, altogether with all the static content such as HTML and JavaScript files. When deployed, all the application code runs in the same machine. Scaling this kind of application usually requires deploying multiple copies of the exact same application code to multiple machines in a cluster, behind some load balancer perhaps.

This design wasn't too bad, we had our applications up and running, after all. But the world, especially when using Agile methodologies, changes fast. Businesses have started asking to release software faster than ever. ASAP has become a very common word in the IT development language dictionary. The specification fluctuates, so the code changes often and grows over time. If the team working on the application is large (and it probably will be in case of complex, huge applications) everyone must be very careful not to destroy each other's work. With every added feature, our applications become more and more complex. The compile and build times become longer, sooner or later it will become tricky to test the whole thing using unit or integration tests. Also, the point of entry for new members coming to the team can be daunting, they will need to checkout the whole project from the source code repository. Then they need to build it in their IDE (which is not always that easy in case of huge applications), and analyze and understand the component structure to get their job done. Additionally, people working on the user interface part will need to communicate with developers working on the middle-tier, with people modelling the database, DBAs, and so on. The team structure will often begin to mimic the application architecture over time. There's a risk that a developer working on the specific layer will tend to put as much logic into the layer he controls as he can. As a result, the code can become unmaintainable over time. We all have been there and done that, haven't we?

Also, the scaling of monolithic systems is not as easy as putting a WAR or EAR in another application server and then booting it. Because all the application code runs in the same process on the server, it's often almost impossible to scale individual portions of the application. Take this example: we have an application which integrates with the VOIP external service. We don't have many users of our application, but then there is a lot of events coming from the VOIP service we need to process. To handle the increasing load, we need to scale our application and, in the case of a monolithic system, we need to scale the whole system. That's because the application is a single, big, working unit. If just one of the application's services is CPU or resource hungry, the whole server must be provisioned with enough memory and CPU to handle the load. This can be expensive. Every server needs a fast CPU and enough RAM to be able to run the most demanding component of our application.

All monolithic applications have these characteristics:

  • They are rather large, often involving a lot of people working on them. This can be a problem when loading your project into the IDE, despite having powerful machines and a great development environment, such as IntelliJ IDEA, for example. But it's not only about the hundreds, thousands, or millions of lines of code. It's about the complexity of the solution, such as communication problems between team members. Problems with communication could lead to multiple solutions for the same problem in different parts of the application. And this will make it even bigger, it can easily evolve into a big ball of mud where no one can understand the whole system any longer. Moreover, people can be afraid of introducing substantial changes to the system, because something at an opposite end could suddenly stop working. Too bad if this is reported by the users, on a production system.
  • They have a long release cycle, we all know the process of release management, permissions, regression testing, and so on. It's almost impossible to create a continuous delivery flow having a huge, monolith application.
  • They are difficult to scale; it typically takes a considerable amount of work to put in a new application instance in the cluster by the operations team. Scaling the specific feature is impossible, the only option you have is to multiply the instances of the whole system in the cluster. This makes scaling up and down a big challenge.
  • In case of deployment failure, the whole system is unavailable.
  • You are locked into the specific programming language or technology stack. Of course, with Java, parts of the system can be developed in one or more languages that run on JVM, such as Scala, Kotlin, or Groovy, but if you need to integrate with a .net library, here begins the trouble. This also means that you will not always be able to use the right tool for the job. Imagine a scenario in which you would like to store a lot of complex documents in the database. They often have different structures. MongoDB as a document database should be suitable, right? Yes, but our system is running on Oracle.
  • It's not well suited well for agile development processes, where we need to implement changes all the time, release to production almost at once, and be ready for the next iteration.

As you can see, monolithic applications are only good for small scale teams and small projects. If you need something that has a larger scale and involves many teams, it's better to look at the alternative. But what to do with the existing monolithic system you may enjoy dealing with? You may realize that it can be handy to outsource some parts of the system outside, into small services. This will speed up the development process and increase testability. It will also make you application easier to scale. While the monolithic application still retains the core functionality, many pieces can be outsourced into small side services supporting the core module. This approach is presented in the following diagram:

In this, let's say intermediary solution, the main business logic will stay in your application monolith. Things such as integrations, background jobs, or other small subsystems that can be triggered by messages, for example, can be moved to their own services. You can even put those services into the cloud, to limit the necessity for managing infrastructure around them even further. This approach allows you to gradually move your existing monolith application into a fully service-oriented architecture. Let's look at the microservices approach.