Microservices Using Traditional Persistence is Challenging


When Java came out, software applications were monolithic, ran in the environment of an application server on a big server machine. Data were processed and stored with relational database systems which are also big monoliths that requiring a big server machine.

Today, Microservices architectures are state of the art, promising agility, high scalability, and resilience in the cloud. When designing microservices, there are some principles to follow:

  1. Each microservice should focus on a single, well-defined business capability.
  2. Microservices should be decoupled, isolated, and should operate independently and communicate through well-defined APIs.
  3. Microservices typically run containerized in their own lightweight container like Docker.
  4. Microservices should start fast to facilitates scaling based on demand.
  5. Each microservice ideally owns and manages its own data storage.

However, the Java community had to realize that meeting these requirements wasn’t possible with the traditional Java stack consisting of the JVM, Java application servers, Java persistence (JPA/Hibernate) and RDBMS. Application servers are designed to run constantly and run multiple applications on the same app server to share common resources. The start-up time is comparable long and the memory footprint is high. In addition to that, the start-up time of the JVM is also comparable long,due to the classloading and JIT compiler optimizations. What has never been an issue for monolithic Java applications became a critical problem with microservices. Suddenly even the future of Java was at stake.

So, to establish microservices in Java, significant changes were necessary. With MicroProfile a working group was founded to create a specification for a new micro runtime to run only a single microservice isolated in its own container. Following from this, the old application servers were replaced by modern microservice frameworks such as Quarkus, Websphere Liberty, Helidon, Payara Micro. With Spring Boot a light-weight version of Spring framework came up to meet the requirements for microservices. With GraalVM it was then possible to transform Java code into a native executable that launches in just a few milliseconds and this minimizes the startup time and memory footprint. Only one component is still the same: Persistence.

To process and store data in a microservice environment, Java developers still use the traditional monolithic Java persistence and monolithic database servers. However, this causes various challenges.

Tight Coupling and Schema Evolution:

JPA often lead to tight coupling between microservices. Changes to the data model in one service necessitate updates in others that access the same data. This hinders independent development and deployment, slowing down innovation and increasing maintenance effort.

Developers spend more time coordinating changes across services, leading to longer development cycles and potential errors due to the complexity of managing intertwined data models. Additionally, testing becomes more intricate and error-prone.

Database Complexity and Scalability:

Managing complex database relationships and joins across multiple microservices using RDBMS can become cumbersome. This complexity can negatively impact performance and increase development effort. Additionally, scaling RDBMS horizontally can be challenging due to shared storage and intricate configuration management.

Developers need to invest significant time and expertise in designing and maintaining complex database structures. Scalability bottlenecks can arise as the number of microservices and data volume grow, requiring costly infrastructure upgrades or potential downtime for configuration changes.

Operational Overhead and Data Consistency:

A central RDBMS server introduces a single point of failure and increases operational complexity. Coordinating distributed transactions across microservices accessing the same RDBMS can be cumbersome and error-prone. Furthermore, maintaining data consistency across services with complex transactions becomes progressively more difficult as the system grows.

Additional staff or expertise might be required for managing and maintaining the central RDBMS server. Development teams need to invest more time in implementing and testing complex transaction logic to ensure data consistency, leading to higher costs.

Statelessness and the Cache-State Dilemma

In a perfect world, microservices should ideally be stateless. This means that each request to a microservice is independent and complete, relying only on the information provided in the request itself. No information about previous interactions is stored within the microservice. This promotes scalability, resilience, and easier deployment.

    JPA/Hibernate was built for object-relational mapping (ORM) to simplify data access and manipulation, because previous Enterprise Java Beans was too complicated. However, achieving usable performance mostly hinges on leveraging caching mechanisms like Hibernate’s first-level (session-scoped) and second-level (application-scoped) caches. These caches store frequently accessed data objects, reducing database round trips and improving performance.

    The crux of the issue lies here. While caching offers performance benefits, it introduces state into the microservice, because a local cache is part of the microservice. Cached data becomes a form of “memory” that the service relies on to respond to subsequent requests more quickly. This deviates from the ideal of complete statelessness in microservices. Moreover, caching increases the need of RAM and the infrastructure costs.

        Cache Invalidation Issues:

        Caching can be a double-edged sword in a microservice environment. Keeping cache entries consistent with the underlying data source in traditional persistence is tricky. Outdated data in the cache can lead to inconsistencies and require careful invalidation strategies.

        Developers need to design and implement robust cache invalidation mechanisms to prevent stale data from causing issues. This adds development complexity and ongoing maintenance overhead.

        Distributed Cache Complexity:

        To improve data consistency in a microservice environment, using a distributed cache is common used strategy. In this scenario, the cache is placed between the microservices and the persistence layer. However, this creates an additional layer between the microservices and the persistence layer. To avoid a single point of failure, the cache is distributed across multiple nodes, which are also stateful and require additional computing resources and significantly more RAM.

        Developers and operation teams need to setup, configure, and maintain a distributed cache solution that causes additional effort of development, testing, and deployment and increases the infrastructure costs due to the additional required computing resources and RAM.

        Connection Management Overhead:

        In a microservices architecture, each service might have its own connection pool to the RDBMS. This can lead to increased overhead associated with managing multiple connection pools across the system. Additionally, connection leakage (forgetting to close connections) can become more widespread due to the distributed nature of microservices.

        Developers and operations teams need to invest more effort in managing connection pools across multiple services. This includes monitoring pool sizes, configuring timeouts, and troubleshooting potential leaks. Additionally, connection leaks can lead to resource exhaustion on the database server, impacting performance and requiring potential downtime to rectify.

        Scalability Challenges:

        Scaling a microservices system with connection pooling can be tricky. If the connection pool size is too small, it can lead to performance bottlenecks as microservices compete for a limited number of connections. However, setting the pool size too large can waste resources on the database server, especially for microservices with infrequent database access.

        Finding the optimal connection pool size becomes a balancing act. Developers need to monitor resource utilization and adjust pool sizes as needed, requiring ongoing tuning and management effort. Additionally, scaling the database server horizontally might be necessary to handle a large number of connection pools, incurring additional costs.

        EclipseStore – High-Performance Persistence for Microservices:

        With EclipseStore there is now a persistence for microservices that eliminates the problems with traditional Java persistence in a microservice environment while providing high-performance data processing and up to 96% database cost savings in the cloud.

        Download: www.eclipsestore.io

                  What are your experiences with persistence in a microservice environment? Do you have the same or other challenges? Please, let a comment and share your experience!

                    Leave a Reply

                    Your email address will not be published. Required fields are marked *

                    Previous Post

                    EclipseStore Kick-Start Days 2024

                    Related Posts
                    Secured By miniOrange