How to write a monolithic application

How to write a monolithic application? Using monolithic architecture has many advantages over microservice architecture. The code is much simpler, the data is consistent, and the application can be tested well. However, when writing a monolithic application, it is necessary to follow some principles. Otherwise, we run the risk of creating a spaghetti code that does not scale well. So what to watch out for?

Level design

The application needs to be divided into several levels. I use API level, Service level, Repository level and Connector level.

  • Repository level – It is only used to access the database. It does not contain any business logic, except in exceptional cases when it is necessary to put this logic into an SQL query/update. The repository sees no other layers. This layer only takes care of the conversion between SQL and JAVA classes.
  • Connector level – Similar to repository level. It is only used to communicate with another system. It should hide third party implementation details and complexity.
  • Service level – It is the core of the whole system, it contains business logic. It sees and uses Repository and Connector levels. It is also in charge of database locking and database transactions.
  • API level – It makes the application available to the rest of the world. The layer should not contain more complex logic. Calls mostly Service level.
Application level architecture

Adhering to this principle is important, otherwise there is a risk of creating spaghetti code.

Multi instances

When writing a backend, one has to think about whether it will run well in multiple instances. This is very important for future scalability.

How to write scalable backed.

Having multiple instances is also important in order to deploy new versions of the application seamlessly. When a new version is deployed, the first instance is turned off first, and after the successful deployment, the second instance is turned off. This procedure can be automated, for example, using a script that controls HAProxy.

We can combine scaling with HAProxy and queue. All nodes contain the same backend code. There are two types of nodes:

  • API (standard) node – accepts a remote connection and processing easy synchronous tasks.
  • Worker node – accepts work from the queue and processing CPU intensive tasks – synchronous or asynchronous (OCR, Excel exports,..)

We can switch from API node to the Worker node by changing configuration settings – typically application.properties file. This simplifies DevOps a lot.

Combination of API nodes and the Worker nodes

Note that backends do not communicate with each other. This is very convenient and simplifies application development and maintenance.

The disadvantage of this architecture is that there is a central database that scales badly. More information how to scale database.

Why microservices are bad

Microservice architecture is not always bad. But in most cases it is.

Why?

Most people do not see the difference between decomposition and network distribution. They usually do a good decomposition and then mistakenly convert it to the network distribution.

Why is it a problem?

Because the network call is difficult:

  1. The network call is slow and unreliable. Really. If you want to decrease your self-confidence, visit my project Spoiler Proxy.
  2. With a network call you loose consistency. It is very hard to implement a distributed transaction. And in the end, you will have to choose between consistency and availability anyway. You cannot have both. See CAP theorem.

If you are not extremely careful, your backend will be inconsistent, slow and sensitive to any component failure. Future refactors will be almost impossible. If the project assignment changes, you will have to rewrite everything from scratch.

But, is it possible to write a scalable distributed backend?

Yes of course. It is not so hard.

How to write scalable backend

Is it possible to write a scalable distributed backend?

Yes of course. It is not so hard. But do not bother with the microservice architecture.

Some tips

  1. Avoid communications between backends. However, there is an exception: Find the places, where CPU is burnt. Typical places like image transformation, OCR, Excel export and so on. These are good candidates for the distribution where you can use a backend to backend call. The price for the network is lower than the price for the computation. You do not have to care about the consistency – if there is some problem, you can compute it again. The computed result is stored or not. Nothing between. Consider using queues instead of direct calls.
  2. Do not use locks on the backend side. Use locks on the database level. Do not use application level locking. Such locking is valid only within one instance and not the entire system. Use the database directly without ORM. You will have more control over SQL.
  3. Do not use HTTP sessions. Use short live cache on the HTTP request instead. It is difficult to synchronize session content from one instance to another.
  4. Assign a unique request ID for each request. You will be able to track calls from the API.
  5. If you want to maintain consistency, stay with a relational SQL database. If you want to really scale the database, use sharding. But be careful, shards should be as independent as possible.
  6. Be careful when writing things like scheduler. We do not want the scheduled task to run multiple times.
  7. Be careful when picking items from the shared queue. If there are multiple consumers, you must ensure that the item is not taken by two instances at the same time.

See scalable monolith design.

Technologies

I used to be a fan of new technologies. Technologies like CORBA, Google Web Toolkit, Hibernate, Angular, BPM Activity and so on. Using these technologies for your project has advantages and disadvantages. After several years of experience, I realized how careful we should be. Especially if the technology does some cool fancy magic. There is no free lunch. In the end, you will pay for it anyway. Maybe not with the money, but definitely with your time.

The typical scenario may look like:

  1. Awareness – You hear about new technology X and you have the temptation to use it.
  2. Learning – You are learning new technology X. You have the feeling you will be able to solve most of your problems using this technology.
  3. Confidence – You are self-confident about the knowledge of the technology X. You are about to add technology to your big project. You are persuing your friends about how technology is great. Everything is nice. Sometimes you create an artificial problem just to solve it by the new technology X. Just to prove it to yourself you need it.
  4. Migration – You want to use the technology X in your big project, so some migration is necessary. It is painful but you know, you are doing it for the future. And the future will be great.
  5. First problems – Strange, you have some use cases which are not covered well by technology X. Also, there are some strange bugs.
  6. Hesitation – Your code is in the production and you spend most of your time searching the internet for fixes instead of writing the new code. You use fixes from stack overflow which you do not understand well. Some software developers leave the project now. They will probably start a new project with the new hot technology Y.
  7. Sobering – There is a new version X 2.0 which is not compatible with version X 1.0 you are using now. Your version is obsolete and not supported anymore. You hear rumors about the technology Y which is better than X. Everybody is using Y now.
  8. Apathy – You do not have time and energy for the migration to X 2.0 nor Y. Most of the time you are fixing problems. It is often a magic. You just hope to not break anything. You are not able to fix some bugs anymore. 

Some useful tips

  1. Study new technologies. Some ideas are great. These technologies are made by clever people.
  2. Before adopting new technology, try to find drawbacks. In which areas is the technology poor? There are always poor areas somewhere. Find them.
  3. Try to solve real problems, not artificial problems.
  4. Try to use new technology in the separated small project first. If you are not in the production you do not have enough experience.
  5. Consider exit plan – will you be able to leave the technology in the future?