The Lego Monolith
The Lego Monolith: a monolith microservice proof of concept
Tim Whitney | May 6, 2016
Over the last several years, microservice architectures have gotten a lot of press. But for many organizations, a full microservice architecture presents a lot of organizational and technological problems. To be “tall enough” to use a full microservice architecture, an organization should have a foundation of the following capabilities:
- Rapid provisioning of new servers. Ideally cloud native.
- Monitoring. Concepts like correlated tracing, circuit-breaking, and self-healing services require a high degree of maturity in monitoring capabilities.
- Rapid application deployment, requiring a culture embedded in DevOps and continuous deployment.
- Autoscaling to scale a single service when load increases.
- Governance around service contracts.
Overcoming these hurdles may not be feasible for all organizations. For those organizations, there’s an alternative option: the Lego Monolith Architecture.
The Lego Monolith Architecture has many of the benefits of a full-bore microservice architecture, without the lengthy requirements. It contains several bounded service contexts within a single repository and deployable application.
An overview of the Lego Monolith Architecture
Many service applications already make use of n-tier architectures to segregate controllers, services, and persistence layers. Here’s a sample diagram to illustrate how this can be done in a standard monolithic application:
With a proper dependencies setup (e.g., in Maven or Gradle), this architecture makes it impossible to do something like call a data access object directly from a REST endpoint. It is still possible to call one service from another, or from a service to a data access object that is completely outside of the scope of that service. These service-to-service calls introduce tight coupling between services. This is one of the main problems that microservices and bounded contexts try to solve.
The following diagram demonstrates a single application with two bounded contexts processing a request to create a sale.
Lego Monolith high-level architecture diagram
Here’s what’s happening in the diagram:
- An external request is received to create a sale.
- A request is made from sales-data-impl to product-web to verify that there is enough inventory to fulfill the sale.
- The sale is created.
- An asynchronous message is published to the queue with the sale information.
- The product service is listening for sale-created messages and adjusts the product inventory.
The operational benefits of Lego Monolith over traditional monoliths
Lego Monoliths offer several operational benefits, including:
- Decoupled design. Each service lives in its own bounded context that can only communicate with other services over decoupled interfaces (e.g., REST or SOAP).
- Choice of language. While the choices aren’t as extensive as a true microservice architecture, each service can be built in any JVM language (e.g., Java, Groovy, Scala, Kotlin, etc.).
- Choice of persistence. Each service should own the data source (e.g., traditional RDBMS, document DBs, flat files, etc.)
- Asynchronous. A message queue can be used for asynchronous communication between service contexts.
- Ease of transition. The Lego Monolith is a good starting point, and individual services can easily be extracted and run as a separate deployable unit with minimal effort.
The development benefits of Lego Monolith over microservice architectures
Lego Monoliths also provide benefits to development, including:
- Domain evolution. It’s hard to organize a larger architecture into a set of microservices with an evolving domain. Having a single code base and a single repository will enable an easier evolution of your services and domain.
- Refactoring. Moving code from one service to another will be far simpler than if you have many separate services in separate repositories.
- Context switching. If one or a small number of teams owns the entire application, working on multiple services is simpler and more intuitive—both from an operational standpoint of switching between repositories and from a mental energy standpoint.
Gradle dependency setup
The root build.gradle file contains subproject dependencies and applies a variety of plugins.
The parent-web build.gradle file contains compile time dependencies to the individual web projects that need to be individually started.
The dependencies within a service are illustrated below for the product modules:
The parent-web module contains the main Spring Boot application entry point.
Spring Boot application configuration
This starts each of the services (sales, support, product) separately. It also includes an ActiveMQ module for purposes of the POC, but should be external in a production environment. The Spring Boot profiles are handled in a more manual way than in a standard Spring Boot project to enable each bounded service to have its own configuration, such as configuring the port and datasource independently. This is the heart of what makes this POC work. The main takeaways are:
- Namespace the individual Spring Boot profiles so that configuration doesn’t leak from one service to another, since the entire application is running on a single classpath.
- Don’t use the standard Spring Boot command line argument to pass the active profile, otherwise Spring Boot will automatically set that as the profile.
Each individual service contains its own Spring Boot configuration.
This extends Spring’s AnnotationConfigEmbeddedWebApplicationContext to support annotation-driven configuration. In the POC, the Spring Boot configuration is consistent across sales, support, and product services; however, there is no reason that this has to be the case.
The ProductApplication uses Spring Boot’s Auto Configuration. The @ComponentScan is configured to only look at packages within the context of this service, otherwise all the Spring annotated beans would be autowire candidates since the entire application is running within a single classpath.
Sales service example
This is the code behind the create sale diagram above.
Sample REST request:
This invokes the SalesController create method:
The method maps the SaleVO to a Sale object and calls the SalesService create method. It returns the created SaleVO object once the Sale has been created.
The service method first calls the ProductDAO to GET the product from the product service in order to check inventory:
The DAO is using Spring’s RestTemplate to make a GET request.
If the inventory is sufficient, then the SalesService calls the SalesDAO to create the Sale.
The SalesDAO uses Spring’s JpaRespository so it’s just an interface that extends JpaRepository.
Once the Sale has been saved to the database (in-memory H2 database in this example), a message is published to the message queue with the information about the Sale.
This is a utility that serializes the object as JSON and publishes it to the given destination.
The ProductService is listening for messages published to this destination using the Spring @JmsListener annotation on the processCompletedSale method.
This method will be invoked asynchronously and relies on eventual consistency to update the product’s inventory.
The source code for the POC is available on GitHub.
Downsides and caveats
The Lego Monolith Architecture isn’t without its drawbacks. Here’s a few to be aware of:
- Choice of technology/language. In a true microservice architecture, individual services can be implemented in a variety of languages (e.g., Java, Node, Python, etc.). The Spring Boot POC restricts the choices to languages that only run on the JVM, and that Spring Boot supports.
- Autoscaling. It is not possible for a single service to scale faster than the other constituent services. However, this architecture supports easy extraction of a single service so that it’s able to run as a separate deployable unit.
- Governance. Considering that individual services still need to talk to each other, this is still an issue that can’t be abandoned. However, this issue is present in almost all distributed service-oriented architecture systems.
- Domain model sharing. Models are shared in this proof of concept. However, there’s no reason that each bounded service couldn’t use contract-first design and share nothing.
- Monitoring. If multiple services are exposed in a public API, then the Lego Architecture does not remove the need for advanced monitoring, such as tracing. However, if most of the APIs are only used internally within the application, then more standard application monitoring can be used.
Due to the high bar that must be crossed to properly implement microservices, adoption of the Lego Monolith Architecture can bring many benefits to a traditional monolithic application architecture, such as a true decoupled design, asynchronicity, and a level of freedom of language and persistence that is not possible with other architectures.