There are a lot of discussions about backend architectures recently. Most of them about how perfect microservices and how bad everything else.
This article definitely not a continuation of these discussion. Instead this is an attempt to debunk some myths and stereotypes as well as provide some insights into backend architecture in general.
First of all, lets refer to basics:
(from Wikipedia)
Software architecture refers to the fundamental structures of a software system and the discipline of creating such structures and systems. Each structure comprises software elements, relations among them, and properties of both elements and relations.
...Software architecture is about making fundamental structural choices that are costly to change once implemented. Software architecture choices include specific structural options from possibilities in the design of software.
From this definition I conclude that while splitting application to services is an architectural decision, how services are deployed is rather technical. While technical decisions may affect architectural ones, they more implementation details rather than architecture itself.
So, is the packaging important? Definitely yes. It affects a lot of decisions during implementation. It has huge influence on the system reliability - location and number of points of failures. It directly affect efforts and expenses of running the application. It may impose limitations and/or enable solutions which are specific to packaging. But the same can be said about technology stack. Anyone considers them a part of the architecture? I don't think so.
So, what's about architecture then?
There are many architectural approaches used in modern web backends and microservices communicating via HTTP. Below I'll try to cover some types of architectures I meet most frequently.
Synchronous 2-layer
This one is simplest and still one of the most widely used one.
Internally it consists of some backend code (layer 1) which receives request, communicates DB (and possibly other services) (layer 2) and sends response back to client. All processing is done synchronously from moment of receiving request to the moment of sending response.
Key value of this architecture is its simplicity for the developer and just enormous set of tools/frameworks/libraries designed to work this way. There are a lot of ready to use tools for this, from classical PHP+MySQL and Spring (more recently Spring Boot)+JPA to countless micro web frameworks.
One of the weakest spots of this architecture (beside poor performance and scalability) is complicated handling of concurrent access to shared resources. In some cases it may introduce yet another bottleneck in the application and make scalability even worse.
Introduction of coroutines and other forms of lightweight thread support may enable these type of architectures to eliminate main shortcoming - quite bad performance and scalability.
Asynchronous 2-layer
This one gained popularity thanks to Node.js, although event-driven programming is definitely not a new concept.
This architecture has the same 2 layers - backend code and DB/other services.
Unlike previous architecture whole processing is performed as a sequence of short steps like "send request to DB", "when response from DB available - format response to client", "send request to other service" and so on. There is no synchronous waiting for the end of the operation, so while there is nothing to do for particular request, underlying framework or platform can handle other tasks.
This type of architecture has very good scalability and performance. Single-threaded server can often outperform multithreaded server written with synchronous architecture. Multithreaded version of this architecture - Vert.x - is among top performers according to Techempower benchmark.
One of the disadvantages of this architecture is that it requires significantly different internal application design. Earlier versions were implemented using callbacks and are well known for "callback-hell". Fortunately introduction of Promises solved this issue, but still requires different mindset from engineer. Another issue is not all "layer 2" components provide asynchronous interface. Fortunately most of widely used DB's already supported and support for other services appears quickly as this type of architecture gaining popularity.
This type of architecture usually has no synchronization-related issues. The event handling code in every callback or Promise action is handled with single thread so there is no need to keep synchronization in mind while writing code in this architecture.
There are other similar approaches, for example Rx* (RxJava, RxJS, etc.) series of libraries, Project Reactor and others. They share same properties, but internal design model is based on streams of incoming events and whole application design is somewhat more declarative.
Overall such architectures are good fit for functional programming style.
Actor-based
This type of architectures based on the concept of Actor.
This type of architecture is less widely used. So far I can remember only couple of frameworks which support it - Rust Actix and Play Framework for Scala/Java. Mostly they tend to mimic interface of described above Asynchronous architectures. One of the sensible differences comparing to Asynchronous architectures is ability to launch actors during handling of the incoming events.
Clustered Asynchronous
This type of architecture is rare but quite interesting. It's similar to asynchronous one, but has no separate DB-layer. Instead DB is the part of the application itself. This is done by embedding Data Grid node (for example, Apache Ignite, Infinispan or Hazelcast) into application.
Being properly implemented this type of architecture has even better horizontal scalability and naturally distributes load by forwarding requests internally to nodes which hold all (or most of) necessary data. Beside scalability this architecture also has very good reliability and availability characteristics. And since there is minimal amount of data transfers across network - low latency.
With microservices approach this type of architecture, unlike all listed above, is less suitable for implementation of individual microservices. Instead it can be used to implement nanoservices and combine benefits of both, microservices and monolith approaches.