Reading time: 13 min
Most IT products for our clients start with modular development.
How to choose an architecture approach for an IT product, compare microservices and modules, and account for implementation risks.
15.4.2020 Microservice development vs customizing the out-of-the-box version and modular architecture. We explore the pros and cons of each approach. 1.
2.
3.
4. Conclusions:
5.
Most IT products for our clients start with modular development.
Some of them eventually evolve into microservices.
In this article, we will look at why this is so and what criteria help decide whether to implement microservices or stick with modules.
Microservice and modular systems are types of IT solution architecture.
When working with modules, the out-of-the-box version of an existing IT product is extended.
An out-of-the-box version is a monolith, a ready-made system with a core that is delivered to all customers in the same form, "as is".
The customization consists of creating modules with the missing functionality.
New modules are created by reusing parts of the monolith, whether the core or other modules. Business logic is implemented inside the monolith: for the program (application, website, portal), there is one entry point and one exit point.
When working with microservices, an IT product is built from scratch and assembled from building blocks - atomic microservices responsible for individual small processes (send an email, fetch order data, change an order status, create a customer, etc.).
A set of such blocks is unified by business logic into a single system (for example, with BPMS).
Despite the relationships between them, each block is autonomous and has its own entry and exit points.
Microservices or modular architecture? You need to make a choice
Out-of-the-box versions exist for all CMS platforms (Bitrix, Magento, Drupal, Hybris, etc.), CRM, ERP, WMS, and many other systems. They sell well and are in high demand. Let us look at why customers most often choose modular architecture and readily buy out-of-the-box solutions.
Fast implementation Installation, configuration, and master data setup for such software take little time. A mid-sized company can realistically start working with the out-of-the-box product in three to four months after launch. For a small business, it can take just a few days.
Of course, this does not apply to high-load projects and enterprise solutions with complex logic.
For many tasks, ready-made functionality is enough Out-of-the-box versions include a lot of built-in functionality. For non-core business tasks, out-of-the-box software is often a good fit. For example, if you need to deploy inventory software, but inventory is not a priority for your business and you do not plan to generate profit from this activity, an out-of-the-box solution will be enough.
Even if small, there is variation. Usually, an out-of-the-box product comes in two or more variants with different functionality. Different variants are selected for different business types, company sizes, and other conditions.
The main issue is that modular systems are not designed for major functionality overrides. They have an out-of-the-box core and ready-made modules, and those are the best things to use. The closer a project gets to enterprise scale and the more complex its customization becomes, the more problems you will have when extending modules. Let us talk about the main ones.
Suppose your project involves complex warehouse logic.
If you choose a modular architecture, developers do not just need to build functionality for managing these warehouses - they need to override or extend the multi-warehouse module, which in turn relies on core methods.
At the same time, you need to account for complex warehouse return logic: dependence on events from the CRM system, moving goods between catalogs, and so on. You also need to account for hidden logic related to refunds, bonus points, and so on.
When so many overrides happen, the monolith changes substantially.
It is important to remember that the relationship between the amount of new functionality and the number of modules is nonlinear: adding one feature requires changes to several modules, each of which affects the others.
Or override a large number of methods from other modules in the new module, which does not change the essence of the problem.
After all the changes, the system becomes so complex that adding further customizations will require an unreasonably large number of hours.
Documentation for modular systems is hard to keep up to date.
There is a lot of it, and it becomes outdated with every change.
Updating one module requires changes to several other documents (user and technical documentation), and they all have to be rewritten.
As a rule, there is nobody to handle such work: spending valuable IT specialists' time on it simply means wasting the budget.
Even storing documentation in code (PHPDoc) does not guarantee its accuracy. In the end, if documentation can differ from the implementation, it inevitably will.
Classic
TDD is hard to use for monoliths because different methods are tightly coupled (five lines of code can easily require 30 lines of tests, plus fixtures).
That is why, when fighting regressions, the functionality has to be covered with integration tests.
But because development is already slow (since it has to be done carefully to account for many overrides), customers do not want to pay for complex integration tests.
Functional tests become as large as they are pointless.
They take hours even when run in parallel. Yes, a modern frontend such as a PWA can be tested through API-level functional tests.
But tests often depend on receiving data from external systems, so they start failing if, for example, the test SAP instance lags N months behind production and the test 1C instance sends incorrect data.
When a small fix has to be shipped for a module, developers must choose between two evils: run the full CI pipeline and spend a lot of time on deployment, or ship a hotfix without running tests and risk breaking something.
It gets rather dramatic when such changes come from marketing on Black Friday. Of course, regression and human error will happen sooner or later. Familiar? In the end, to meet business goals, the team switches to emergency mode, skillfully juggles tests, and closely watches dashboards built from logs - Kibana, Grafana, Zabbix...
Burnout. Admit it, this kind of regression situation does not look like a "stable enterprise"
, as it should be in the customer's dreams and aspirations.
Another problem caused by code coupling is the difficulty of updating the platform. For example, Magento contains two million lines of code.
Wherever we look, there is a lot of code everywhere (Akeneo, Pimcore, Bitrix).
When adding functionality to the core, it is best to account for changes in your custom modules.
Let us take Magento as an example, as similar cases exist in any modular system on large projects. At the end of 2018, a new version of the Magento 2.3 platform was released. Multi-source inventory and Elasticsearch were added to the Open Source Edition.
In addition, thousands of bugs were fixed in the core, and some nice improvements were added to OMS. What did e-commerce projects face when they had already built multi-warehouse setups on Magento 2.2?
They had to rewrite a lot of logic in order processing, checkout, and the product page to move to the out-of-the-box functionality.
After all, this is the "right" way - why duplicate out-of-the-box functionality in modules?
Reducing the amount of custom code in a large project is always useful, since all out-of-the-box methods account for exactly these multi-warehouses; updating the product without such refactoring may be pointless (leaving security issues aside for simplicity, especially since they can be applied without upgrading).
Now imagine how long such an update would take.
How can this be tested without integration tests, which are hard to write? It is no surprise that for many, platform updates happen either without refactoring, but with more duplication, or, if the team wants to do everything properly, with a long detour into refactoring and cleanup.
One of the most important problems in project management is that the client does not see the full logic and all business processes of the project.
They can only be reconstructed from the code or from documentation, whose accuracy, as mentioned earlier, is difficult to maintain in modular systems. Yes, Bitrix has a BPM component, and Pimcore has workflow visualization.
But this attempt to manage modules through business processes always conflicts with the existence of a core.
In addition, events, complex timers, and transactional operations cannot be fully supported in a modular architecture system.
To repeat once again, this applies to medium and large companies.
For a small company, the capabilities of modular systems will be enough.
But if we are talking about the enterprise segment, this solution is noticeably missing a single control center where you can open any process or status on a diagram and see exactly how something happens, what exceptions there are, and which timers, events, and cron jobs exist.
There is no way to change business processes instead of modules.
Process-based project management is overwhelmed by the pace of change and logic coupling.
If you deploy a monolith (for example on Magento, Bitrix, Akeneo, Pimcore, OpenCart, etc.), it is deployed in full, with all modules, on every app server. It requires more memory and CPU, which significantly increases cluster cost.
Spoiler: the issues listed in the previous point can be solved with BPMS and orchestration of microservice systems. BPMS (business process management system) is software for managing business processes in a company.
Popular BPMS platforms we work with include Camunda and jBPM.
Orchestration describes how services should interact with one another by exchanging messages, including business logic and the sequence of actions.
Using BPMS, we do not just draw abstract diagrams - the business process will execute exactly as drawn. What we see in the diagram is guaranteed to match how the process works, which microservices are used, what parameters are passed, and which decision tables are used to choose one logic path or another.
The example used here is a common process - sending an order to delivery.
Example of an order delivery process flow
Using some message or a direct call, we start order processing along with the delivery method selection process.
As a result, processes, services, and development become easy to read, self-documenting (they work exactly as drawn, with no mismatch between documentation and the actual code behavior), and easy to debug (it is simple to trace any process and understand where the error is).
Let us look at the principles that business process management systems follow.
BPMS makes it possible to create a business process in which the project team (developer or business user) defines the sequence for launching microservices, as well as the conditions and branches that guide its flow.
At the same time, one business process, meaning a sequence of actions, can be included in another business process.
All of this is clearly represented in the BPMS: in real time, you can view these diagrams, edit them, and deploy them to production.
The principle of a self-documenting environment is applied here to the fullest extent - the process works exactly as visualized!
All microservices become process building blocks that can be added from the toolkit by a business user.
The business controls the process, and the developer is responsible for the availability and correct operation of a specific microservice.
At the same time, all parties understand the overall logic and purpose of the process.
The principle sounds very simple, and an inexperienced developer or business user may think that a BPMS does not improve the microservice design strategy itself. In other words, you can write decent microservices without a BPMS. Yes, you can, but it is difficult.
When a developer writes a microservice without BPMS, there is inevitably a temptation to cut back on abstraction.
Microservices become outright large, and sometimes even start reusing others.
There is a tendency to cut corners on making the transfer of results from one microservice to another transparent. A BPMS encourages more abstract code.
Development is carried out in a process-driven way, with inputs and outputs defined.
Imagine an order processing workflow: we need to go to a marketplace, collect all the good orders, and start processing them. Order queue processing diagram Take a look at the diagram (a portion of it).
Here it is defined that every 10 minutes we check all marketplace orders, then run the processing of each order in parallel, which is indicated by the vertical "hamburger" in Process Order.
If successful, we send all data to the ERP and finish processing.
If we ever need to pull logs for processing a specific order, in Camunda, JBoss, or any other BPMS we can fully restore all the data and see which queue it was in and what input and output parameters were used.
Imagine that an error occurred during product delivery.
For example (by the way, this is a real case), the shipping company picked up the order, and after that the warehouse burned down.
Another real story: the pre-New Year rush, and the goods were first delayed and then perhaps lost. In such a case, events are clicked together in the BPMS with the mouse, for example a customer notification if the delivery time has passed.
If we receive an error from the shipping carrier, we can trigger the process down our own branch and stop everything: notify the customer, offer a discount on the next order, or issue a refund.
Such exceptions are hard not only to program outside the BPMS (for example, a timer within a timer), but also to understand in the context of the entire process.
Imagine the same delivery order.
In total, we have three possible outcomes: the item was delivered on time; the item was not delivered on time; the item was lost.
In BPMS, we can define the procedure for handing goods over to a carrier and then wait for one of two events on the order: successful delivery (messages from the delivery process confirming that everything has been delivered) or a time-based trigger.
If the time has not passed, you need to launch another service: a review with an operator of this specific order (you need to create a task for them in the OMS/CRM system to investigate where the order is), followed by notifying the customer.
But if the order was delivered during the investigation, the investigation must be stopped and order processing completed. In BPMS, all interruptions and exceptions are handled by BPMS itself.
You do not overload the code with this logic, because having such logic in the code would make microservices large and poorly reusable in other processes.
With events and the inter-process option, you can see the entire event sequence in one window (it is easy to see what happened to the order, which exception path it followed, and so on); you can gather all BI analytics based only on BPMS logs, without overloading microservices with logging events. As a result, you can build statistics specifically on processing issues, transition speeds, and all company processes.
Logging data becomes standardized, making it easy to link a delivery event with an operator action or with an event in any other information system.
Note the difference from a modular system.
Universal logs can also be implemented there.
But when interacting with other systems, you will also need to do something about log standardization there, and that is not always possible.
Each architecture type has its own advantages and disadvantages. There is no universal solution.
For small businesses or when using only a very small amount of customization, the modular approach will be more suitable.
It is important to look for the right technical solution, not promote a specific platform or approach. Let us look at how different architecture types change depending on a project’s lifespan.
At the start of a project: with microservices, you have no functionality at all, and all of it must be built before you can begin work; with a modular system, the out-of-the-box version gives you a large amount of functionality right away, and you can start using the product shortly after purchase.
After the first three to four months of development (the average time to release the first MVP) and beyond: with microservices, the amount of functionality gradually catches up with the out-of-the-box version.
For mid-sized businesses, a microservice architecture will catch up with a modular one fairly quickly, and for large businesses, almost immediately. Over time, the cost of maintaining and extending a modular system per unit of functionality will rise; with a modular system, the speed of feature development will be much lower than with microservices. Finally, let us look at what microservice orchestration looks like in concrete examples.
Let's look at service orchestration using Camunda.
The following images will help you see how convenient it is to manage microservices with a BPMS and an orchestrator.
All processes are visual, and the logic is obvious. Business processes look like this:
Business process diagram in Camunda (BPMS with orchestration) Example (order, availability service):
Business process demo in Camunda: order, availability service. It is visible that this order had the "No stock" branch.
Another order instance (sent to picking):
Business process demo in Camunda: order sent to picking
The order moved on and, according to the decision table (DMN), went into the handling branch for a specific delivery operator (Boxberry):
Business process demo in Camunda: order handling by a delivery operator Transition to the nested process:
Business process demo in Camunda: nested process The nested process completed:
Business process demo in Camunda: nested process
Business process execution history:
Business process demo in Camunda: history
Properties of this kind of visualization: business processes are easy to read even for an untrained user; they are executable, meaning they work exactly as drawn; there is no mismatch between the "documentation" and the actual code behavior; processes are transparent: it is easy to see how a particular import, order, or processing step went through, and easy to understand where an error occurred. See also: