Hey, Java devs! We all know how pivotal it is to choose the right Java framework for a project’s success. I’ve been exploring various Java stacks to find out favorites among developers and the specific reasons behind their choices.
Hey, Java devs! We all know how pivotal it is to choose the right framework for a project’s success. I’ve been exploring various Java stacks to find out favorites among developers and the specific reasons behind their choices. Whether it’s Spring, Hibernate, Quarkus, or another framework entirely, I’ve delved into features and benefits to determine the preferred one.
Here are some of the requirements for the ideal stack selection:
- Performance and Scalability
- Well supported
- Observable by default
- Community Support
- Adaptability and Flexibility
- Speed of Development
- Project Requirement
By the way, I’ve embraced a minimalist approach and have discontinued my use of Lombok and Cucumber since JUnit 5, Quarkus, and IntelliJ fulfill my needs. I’m content with this stack primarily due to its observability capabilities.
Here are my Go-To Frameworks and tools:
1. Java Development Kit (JDK) 21
I didn’t want to be stuck with the past of Java, unlike most companies still using Java 8 and 11. I will show you why I decided to use Java 21 and why you should use Java 21 instead of Java 8 and 11.
Java 21 comes loaded with eight permanent JEPs (JDK Enhancement Proposal), which are:
- Sequenced Collections
- Generational ZGC
- Record Patterns
- Switch Pattern Matching
- Virtual Threads
- Deprecation of the Windows 32-bit x86 Port (marked for Removal)
- Prepare to Disallow the Dynamic Loading of Agents
- Key Encapsulation Mechanism API
For the purpose of the application I’m building, I’m only interested in the following features in Java 21: switch pattern matching and virtual threads.
Switch Pattern Matching
The development of switch control structures has undergone significant advancements since their initial introduction. Conceptualized during Java version 17, the notion of using patterns to match switches has been fine-tuned through subsequent iterations of the JDK. With Java version 21, this innovative feature has become a fully-fledged (non-preview) feature.
Pattern matching deals with the limitations of traditional switch in several ways:
- The type of the selector expression can vary as a reference type or an integral primitive type, excluding the long data type.
- Case labels can contain both patterns in addition to constants.
- Case labels can include null.
- An optional “when clause” can follow a case label for conditional or guarded pattern matching.
- Enum constant case labels can be qualified. The selector expression doesn’t have to be an enum type when using enum constants when using enum constants.
- The introduction of the `MatchException` allows for a more consistent and organized way of handling errors during pattern matching.
- The traditional switch statements and the traditional fall-through semantics also support pattern matching.
Virtual Threads
The main reason a lot of people, including I are switching to Java 21 is because of virtual threads.
In Java, virtual threads operate similarly to goroutines in the Go programming language. It allows for the execution of multiple IO-bound tasks concurrently within a program. It allows for the efficient use of system resources by dividing work into smaller, manageable pieces that can be executed simultaneously, thereby improving overall performance and responsiveness.
Before now, to carry out concurrent IO-bound operations like reading/writing files, network communication, database queries, socket operations, and reading from standard input, we had to use reactive programming. However, reactive programming came with its challenges:
- It is memory intensive due to the fact that it has to store streams of data most of the time
- It can feel very unconventional to learn at first, and that’s because it needs everything to be a stream.
- Most of the complexities you’ll encounter have to be dealt with at the time of the declaration of a new service
- It has a steep learning curve.
- It is often confused to be taken as Functional Reactive Programming.
Virtual threads solve all of these challenges with reactive programming, and it is so easy in the sense that you’ll need little or no code change.
How Does Virtual Threads Work?
Here is the thing: virtual threads still run code on OS threads. However, when the code running in a virtual thread calls a blocking I/O bound operation, the Java runtime temporarily halts the execution of the virtual thread until the I/O operation gets completed. During this time, the OS thread associated with the paused virtual thread becomes available to handle tasks related to other virtual threads.
How Do You Create a Virtual Thread?
I discovered that you create virtual threads just the same way you create platform threads. All APIs designed to function with platform threads can seamlessly integrate with virtual threads without any modifications. However, there are differences in how you create Java virtual threads compared to platform threads. Here’s a code snippet demonstrating how to create a Java virtual thread:
Runnable task = () -> { System.out.println("Hello Digma!"); }; Thread.startVirtualThread(task);
Advantages of Virtual Threads
Virtual threads offer numerous benefits due to its elegant design or architecture:
- Lowers the amount of memory used by applications.
- Your applications are easier to use.
- Enhances software performance by accelerating the processing of requests or tasks.
- Mitigates ‘OutOfMemoryError: unable to create new native thread’
- You get an improvement in your code quality
- Fully compatible with Platform Threads
Now, here is the sweet spot: because I’m using Quarkus, I don’t need to do any configuration to use Virtual threads. Quarkus uses virtual thread out of the box.
2. Quarkus
I know Spring Boot has a wide adoption, making it the most adopted Java web framework. However, I decided to use Quarkus as my go-to framework. In this section, we’ll uncover the power of Quarkus, a Kubernetes-native Java framework. I’ll show you how it streamlines the development of fast, small, and efficient microservices and serverless applications.
What is Quarkus?
Quarkus is an open-source project licensed under the Apache License version 2.0, and it is a Kubernetes-native Java web framework built to optimize Java specifically for containers.
Quarkus follows a “container-first” approach focusing on being native to Kubernetes, prioritizing minimal resource consumption and rapid start-up times. As part of this design, only code that directly impacts runtime performance is executed by the Java Virtual Machine (JVM), while other parts remain unloaded.
I’m sure with the definition above, you are already seeing why I fell in love with Quarkus and decided to use it as my go-to framework. Below are the things I get to enjoy while using Quarkus:
- Low memory footprint.
- The hot reloading feature (in dev mode) is awesome.
- It is fast (faster than Spring Boot).
- Shallow stack traces (unlike Spring boot).
- It uses AOT compilation making it run and work faster, using few resources.
- It significantly streamlines the process of working with containers, particularly when it comes to using Kubernetes, by eliminating redundant processes in configuring and deploying applications.
- You can write imperative code or reactive code.
- Plenty of modules and extensions
- Compile-time wiring (unlike Spring Boot’s runtime wiring).
- Fast startup (kubectl deploy restart gets executed in less than a sec).
- The Qute template engine is awesome.
- Low memory space requirements for native images.
- Since it is Sponsored by Redhat, it makes it very easy to convince senior mgmt.
Below is a code sample of what Quarkus looks like:
@ApplicationScoped @RunOnVirtualThread @Path("/api/person") @Produces("application/json") @Consumes("application/json") public class PersonResource { @Inject IPersonService personService; @GET public List<Person> get() { return personService.getAll(); } @GET @Path("{id}") public Person getSingle(@PathParam("id") UUID id) { return personService.getById(id); } @POST public Response create(Person person) { return personService.create(person); } @PUT @Path("{id}") public Response update(@PathParam("id") UUID id, Person person) { return personService.update(id, person); } @DELETE @Path("{id}") public Response delete(@PathParam("id") UUID id) { return personService.delete(id); } }
Observing Quarkus
It is not just enough to use these frameworks; it is important to know how they behave at runtime; that way, those who will use your application don’t get to point out things you should have noted while developing.
I used Digma because of my challenges with other observability tools. But you can use whichever tool you desire.
Most of the observability tools:
- do more monitoring than observing
- have a relatively steep learning curve.
- lack the capacity to track transactions through complex distributed systems.
- are not cost-effective
- have poor documentation
The Digma plugin for me, was a guarantee that I write efficient code and leverage it to identify and address performance issues, scaling problems, and query bottlenecks in even the most complex areas of my codebase.
I’m using IntelliJ IDEA with the Digma plugin installed and enabled for this project I’m working on. One of the neat things about Digma is that it takes care of importing any dependencies. Clicking the auto-configuration link will add the Quarkus OTEL extension as a dependency.
Why Use OpenTelemetry Library for Quarkus?
I’m using this library because OpenTelemetry (OTEL) is the de-facto standard in the industry. It allows developers and companies to develop services in different programming languages and unify the telemetry collection and analysis.
With the OTEL extension, Quarkus automatically exports all monitoring data (telemetry) utilizing the Observability Trace Language Protocol (OTLP). This is done by default for OpenTelemetry traces and can also be achieved for Micrometer metrics through integration with the Quarkiverse’s OTLP registry.
OTel tracing works in Quarkus with little or no configuration, and implementation is done automatically for imperative and reactive code in many different extensions and clients, like vertex, Kafka, Resteasy, JDBC, and so on.
There are many benefits to using observability when you develop test, and run your code in production. Given the right tools (even free tools like Jaeger and Digma), developers can detect code issues much earlier and enjoy unprecedented insights into how complex code runs.
If you’re interested in more info about Continuous Feedback and the practice of using observability in coding, I heartily recommend this article as it also covers a few examples.
Runner up: Spring Boot
Spring Boot is a web framework built on the top of the Spring Framework. It provides an easier and faster way to set up, configure, and run simple and web-based applications.
Spring Boot is very popular amongst Java developers because it has:
- Starters for every possible thing
- Embedded HTTP servers
- built-in observability
- Reduced boilerplate code
- A large community of users
- No need for XML configuration
- Reduced development time and increased productivity
Below is a code sample of a Spring Boot application:
@RestController @RequestMapping("/api/person") public class PersonController { private final PersonService personService; @Autowired public PersonController(PersonService personService) { this.personService = personService; } @GetMapping public ResponseEntity<List<Person>> getAll() { List<Person> persons = personService.getAll(); return ResponseEntity.ok(persons); } @PostMapping public ResponseEntity<Person> create(@RequestBody Person person) { Person createdPerson = personService.create(person); return ResponseEntity.status(HttpStatus.CREATED).body(createdPerson); } @GetMapping("/{id}") public ResponseEntity<Person> getById(@PathVariable UUID id) { Person person = personService.getById(id); return ResponseEntity.ok(person); } @PutMapping("/{id}") public ResponseEntity<Person> update(@PathVariable UUID id, @RequestBody Person person) { Person updatedPerson = personService.update(id, person); return ResponseEntity.ok(updatedProduct); } @DeleteMapping("/{id}") public ResponseEntity<Void> deletePerson(@PathVariable UUID id) { personService.delete(id); return ResponseEntity.noContent().build(); } }
Observing Spring Boot
Carrying out observability in Spring Boot is straightforward, and that is because it comes with an in-built capacity for observability. You can observe Spring Boot apps using the OTEL agent or by enabling Micrometer and the Actuator. While the agent delivers more out of the box and doesn’t require any code changes to try it out – the Micrometer approach is more built-in and doesn’t carry the same performance cost.
If you’re using Digma, it will automatically add the required dependencies for distributed tracing, similar to Quarkus. You will also be able to choose which strategy you prefer to use: the OTEL agent on Micrometer.
3. Maven
I get it; Maven is slow compared to Gradle; at least, that is what you hear. However, against popular opinion, I decided to use Maven, the tried-and-true build automation tool. In this section, I will show you how to orchestrate your project’s build lifecycle seamlessly and get it to be as fast as Gradle.
What is Maven?
Maven is basically an automation build tool for Java-based projects. Its widespread adoption within the Java developer community speaks to its effectiveness in streamlining various aspects of project development, including dependency management, building, testing, and deployment.
Maven shines in dependency management, allowing developers to manage the various dependencies required for their projects easily.
With Maven, managing project dependencies is streamlined by storing all relevant details regarding a project and its dependencies within a centralized file – the `pom.xml` located in the project’s root directory. This file includes information such as the version numbers for each dependency, which Maven utilizes to automatically retrieve and set up the appropriate versions of these dependencies without manual intervention. As a result, dependency management becomes significantly more straightforward, especially when dealing with complex projects comprising numerous dependencies.
For building and deploying projects, Maven provides a standard build lifecycle and a directory layout, making it seamless to build and deploy projects. It also comes with a wide range of plugins used to perform common tasks like building, code compilation, running tests, and project packaging for deployment.
Furthermore, Maven also provides a variety of tools to carry out activities like automated testing, code analysis, code coverage, and much more. Maven also allows easy integration with other tools, such as CircleCI, GitHub Actions, TravisCI, Jenkins, etc, for Continuous Integration and Deployment.
Why is Maven My Preferred Build Tool?
I have used Maven, I have used Gradle and I must say they are all good build tools. A lot of developers will go for Gradle because of speed and because they don’t want to write XML; I mean, who likes writing XML? However, down the line, you’ll start hitting the following drawbacks:
- Steep learning curve: It is either the whole team learns Gradle, or you guys get to depend on a single Gradle ninja in your team, and believe me, that is not a good position to be in as a team.
- Constant API change: Due to the dynamic Groovy DSL (Domain-Specific Language) and the heterogeneous plugin APIs, you will always see yourself google everything.
- A lot of surprises: In programming, no one loves surprises, even if they are pleasant. With Gradle, get ready for unexpected behaviors, side effects and interdependencies between plugins.
- New Gradle versions more often than not, come loaded with breaking changes that affect existing plugins, and the maintainers of most of these open-source-projects can’t keep up with this speed.
- As Groovy is dynamically typed, it can be hard for some IDEs to provide efficient and fast tooling, unlike Maven, where parsing and interpreting its XML is dead simple.
Seeing all of these drawbacks, I decided to stay put with Maven and when build speed is a concern to me, I switch to Mvnd.
Seeing all of these drawbacks, I decided to stay put with Maven and when build speed is a concern to me, I switch to Mvnd. Below is what a sample Maven pom.xml file looks like:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>gemstar</groupId> <artifactId>gemstar</artifactId> <version>0.0.1</version> <properties> <maven.compiler.release>21</maven.compiler.release> <!-- Add other necessary properties --> </properties> <dependencies> <!-- Add your dependencies here --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-arc</artifactId> </dependency> <!-- Add other dependencies as needed --> </dependencies> <build> <plugins> <plugin> <groupId>io.quarkus</groupId> <artifactId>quarkus-maven-plugin</artifactId> <version>3.5.0</version> <extensions>true</extensions> <executions> <execution> <goals> <goal>build</goal> <goal>generate-code</goal> <goal>generate-code-tests</goal> </goals> </execution> </executions> </plugin> <!-- Add other necessary plugins --> </plugins> </build> </project>
4. Postgres
Out of the numerous RDBMS (Relational Database Management System) out there, I decided to go for Postgres. This section will show you why Postgres is my go-to relational database. I’ll be showing you the robust features and reliability for data persistence that it has to offer.
After the acquisition of MySQL by Oracle, it began to lose popularity amongst developers; hence, developers started looking for other open-source alternatives. PostgreSQL took center stage as an ORDMS (Object Relational Database Management System).
This section is not for me to do a comparison between PostgreSQL and MySQL, but rather to tell you why I went for PostgreSQL:
- It is fully SQL-compliant
- It is ACID-compliant
- It comes loaded with object-oriented database features
- I can write database functions using SQL, Tcl, Perl, Python, Java, Lua, R, shell, and Javascript.
- It allows me to define my types: Defining them helps me align my database more closely with how my data is represented in my applications.
- It allows me to create views, allowing for convenient, simplified access to data as it abstracts the original table structures for frequently queried data.
- Support for a large number of data types, including common database primitives (numeric, string, boolean, and DateTime types) and complex types like network addresses, geometric types, monetary types, Ranges, JSONB, hstore
- It has a feature called Write-Ahead Logging, which provides point-in-time recovery, failover, and streaming replication.
- It comes with a full-text search feature, which gives me powerful techniques for finding and operating on data in semi- and unstructured text. My search can be fine-tuned to match things that are relevant and match.
5. JUnit 5 with Parameterized Tests
I know many Java projects still use JUnit 4; however, I decided to elevate my testing game with JUnit 5. In this section, I’ll show you how you can leverage the new features that JUnit 5 comes with so that you’ll be able to write parameterized tests and ensure comprehensive and scalable test coverage.
The importance of testing can not be overemphasized, in the same vein, using the right tool to test can not equally be overemphasized. JUnit 5 has been a buzzword in the Java community despite the emergence of TestNG.
What is JUnit?
JUnit is an open-source testing framework that provides annotations, assertions, test runners, and reporting tools for writing and executing automated tests in Java. It follows the xUnit architecture, a common pattern for writing test cases and suites.
I’m not using JUnit 5 because it is a dominant testing framework in the Java community. I’m using it as my go-to framework for test because of the following reasons:
- JUnit 5 offers me a simpler approach to carrying out unit tests
- Unit tests are easy to implement with fewer lines of code.
- It provides helpful annotations that simplify the process of creating test cases, enabling users to make changes more easily.
- It comes with an integrated support for assertions, which enable more succinct and precise testing procedures.
- JUnit 5 is organized into multiple libraries, so it allows developers to import only the features they need into their projects. With my preferred build tool like Maven, including the right libraries is seamless.
- Due to its support for third-party assertion libraries, I can use my favourite assertion library.
- I can integrate JUnit 5 with any commonly used testing libraries and frameworks.
- I can equally extend JUnit 5 and write the features I need.
- Using parameterized tests, I can remove duplicate code.
Where do Parameterized Tests Come in?
Parameterized test is a handy feature that enables you to run the same test case multiple times with distinct input values. Doing so can save time and keep your codebase organized by minimising the need for individual test methods for every possible input combination. With parameterized testing, you can define a single test method that accommodates various input values, making your test code more concise and easier to maintain over time.
JUnit 5 does not support parameterized tests out of the box. To enable this support, I had to add the following to my pom.xml file.
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> <version>5.9.0</version> <scope>test</scope> </dependency>
6. Testcontainers
Before the advent of Testcontainers, I usually used in-memory databases to test how my application would interact with the database; however, this approach came with many disadvantages as the SQL queries used were incompatible with the ones used in production. So, in this section, I’ll show you how TestContainers solves this problem.
I will show you how to spin up Docker images during tests, ensuring consistent and reproducible testing environments.
What is Testcontainers
Testcontainers is an innovative software testing library designed to facilitate seamless integration testing through lightweight and efficient APIs. By leveraging Docker containers, this tool enables developers to create robust tests that interact directly with actual external services used in production environments, eliminating the need for cumbersome mocking or in-memory simulations. With Testcontainers, users can easily craft tests that mirror their production scenarios, ensuring comprehensive test coverage and increased confidence in the quality of their applications.
In order for me to make use of Testcontainers I had to add the following dependencies to my pom.xml file.
<dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers</artifactId> <version>1.12.5</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>1.12.5</version> <scope>test</scope> </dependency>
Incorporating observability into testing processes significantly improves the testing experience by offering valuable insights into an application’s behavior and performance. Observability tools enable testers to closely monitor various aspects, such as response times, error rates, and resource utilization, during test executions in real time.
This enhanced visibility allows for quicker identification of bottlenecks, potential issues, or unexpected behavior, enabling more effective debugging and diagnostics. By integrating observability into the testing process, teams can proactively address performance concerns, ensure system reliability, and deliver a more robust and resilient software product.
Conclusion: What’s Your Go-To Java framework
As an experienced developer, I rely on this collection of frameworks to build robust, efficient, and reliable web applications. These frameworks are my trusty companions throughout my web development process, enabling me to create scalable and fault-tolerant applications. Every element within this stack is vital; they work together seamlessly to guarantee the success of my projects. There you have it; these are my go-to frameworks.
Frequently Asked Questions
What frameworks go with Java?
- Spring
- Quarkus
- JUnit 5
- TestNG.
- Hibernate.
- Struts.
- Vert.x
- Grails.
- Micronaut.
- Helidon
- Vaadin
- Play
What is an example of a framework in Java?
Quarkus is an open-source project licensed under the Apache License version 2.0; it is a Kubernetes-native Java web framework built to optimize Java specifically for containers.
What are Java frameworks and libraries?
Java frameworks are collections of pre-existing software components designed to facilitate the development of Java applications more efficiently and rapidly. These components include pre-written Java code, classes, templates, components, and various structural elements that serve as a foundation for creating an application. By leveraging these frameworks, developers can streamline their work process and reduce the time required to build and deploy high-quality Java applications.