Our Java to Golang journey
Java: We really had a dream run, old buddy
Java-based microservices, particularly those utilizing Spring Boot, have long been celebrated for their robust features and extensive community support. Spring Boot’s convention-over-configuration approach simplifies the deployment and development of microservices, offering a plethora of out-of-the-box features like auto-configuration, standalone capability, and easy dependency management that make it a go-to choice for many developers. The ecosystem provides a mature, well-documented pathway for building resilient and scalable services. However, despite these substantial benefits, some pain points persist, such as longer startup times, memory consumption, and the complexity that comes with managing a JVM-based application, which can hinder performance and cost efficiency in cloud environments.
In a final effort to maintain our Java ecosystem, we experimented with Quarkus, hoping its promise of optimized runtime and reduced memory footprint would address our concerns. Quarkus, with its ability to compile applications into native executables, seemed like a beacon of hope. However, the reality was a tangle of complexities. The process to make our application natively compile was fraught with numerous adjustments and workarounds. Even more frustrating was the fragile nature of the native build; a single third-party library, not fully compatible with Quarkus’s native image, could dismantle the entire setup, forcing us back to running it as a standard JDK application. This precarious balance between performance gains and operational stability highlighted the need for a more streamlined, less constraining solution.
Our requirements: We need a hero
We required a solution capable of swiftly building native images, encompassing all necessary microservice components within the SDK itself. It needed to boast impressively quick start times, be easily packaged into a Docker image derived from scratch, and maintain a lightweight footprint, with both container image size and RAM usage under 100 MB.
Golang: Streamlining Microservices for Efficiency and Reliability
Enter Golang, a language designed with modern computing challenges in mind. Unlike the complexities and overhead associated with Java and the precarious nature of native compilations in Quarkus, Golang offers a straightforward, efficient approach to building microservices. Its compilation to native binaries eliminates the need for a JVM, drastically reducing memory footprint and startup times. With its strong concurrency support and simple, clean syntax, Golang simplifies the development process and enhances performance. Furthermore, its robust standard library and minimalistic nature mean fewer dependencies and a lower chance of compatibility issues. Golang not only addresses the pain points mentioned in the previous paragraphs but also propels applications into a realm of greater efficiency and reliability.
How we planned the migration
Initially, we dissected our business into various subdomains following Domain-Driven Design (DDD) principles, aiming to assign a dedicated microservice to each. Specifically, we focused on creating distinct microservices for HR management, Financial management, and Asset management. Next, we identified the need for shared kernel microservices to manage domain entities utilized by multiple subdomains. Collaboratively, we delineated these domain entities among the microservices using a whiteboard, ensuring a clear and logical distribution.
To visualize and better organize our architecture, we crafted a dependency graph diagram. This diagram stratified the microservices into levels based on their interdependencies, with shared kernels forming the foundational layer and the more isolated, subdomain-specific microservices occupying the higher tiers.
The final step in our preparatory phase involved compiling a comprehensive list of use cases for each microservice. We meticulously mapped these against the test cases and existing use cases in our legacy backend, ensuring a thorough and seamless transition to our new, more efficient microservice architecture.
How we executed it
We opted for a monorepo approach, organizing all our microservices as separate folders under a single repository named ‘ ems-backend.’ Furthermore, we crafted and open-sourced a simplistic framework-style package. This package facilitates the implementation of Golang microservices, complete with an HTTP server and Mongo database, while adhering to the principles of clean architecture. Utilizing our framework, the primary task is to develop use cases as functions for commands and queries within your system. We did, however, write custom handlers for specific HTTP tasks like file uploads. In our setup, use cases are the core aspect of the backend, orchestrating multiple domain entities to address user needs effectively.
Building upon our success, we’ve also developed an exceptionally fast API gateway using Go. This gateway performs edge authentication adhering to the OWASP microservices security standards. Each microservice is equipped with its own middleware — a straightforward function that verifies the incoming request URL against the ROLES embedded in the Passport token. This ensures a secure, efficient interaction between services.
Our achievements after the move
We revised our Kubernetes manifests to deploy our applications, reducing the RAM limits from 500 MB to a mere 50 MB. The transformation was remarkable: our 32 GB backend now operates smoothly on an 8 GB cluster, utilizing just 2 GB of RAM. That night, we celebrated our success with an abundance of pizza. If you’re currently managing JDK applications with a RAM limit of 2 GB or more, our story might shine a beacon of hope your way. Together, we can achieve similar efficiency. Let’s embark on this journey — contact us
We are actively developing software in the space of business solutions, hence you will be surprised to see the speed of our innovation and execution. Follow us on LinkedIn for updates.