The Quarkus framework: how clean architecture is implemented in it

Hello, Habr!

Continuing to explore new Java frameworks and considering your interest in book About Spring Boot, we are looking at the new Quarkus framework for Java. You will find a detailed description of it here, and today we propose to read the translation of a simple article demonstrating how convenient it is to use Quarkus to adhere to clean architecture

Quarkus is rapidly gaining the status of a framework that cannot be avoided. Therefore, I decided to go through it once again and check to what extent it disposes to adhere to the principles of Pure Architecture.

As a starting point, I took a simple Maven project that has 5 standard modules for building a CRUD REST application following clean architecture principles:

  • domain: domain objects and gateway interfaces for these objects
  • app-api: application interfaces corresponding to practical cases
  • app-impl: implementation of these cases by means of the subject area. Depends on app-api and domain
  • infra-persistence: Implements gateways that allow the domain to interact with the database API. Depends on domain
  • infra-web: Opens the considered cases for interacting with the outside world using REST. Depends on app-api

In addition, we will create a module main-partition, which will serve as the deployable application artifact.

When planning to work with Quarkus, the first step is to add the BOM specification to your project’s POM file. This BOM will manage all versions of the dependencies that you use. Also you will need to configure standard plugins for maven projects in your plugin management tool like plugin surefire… As you work with Quarkus, you will also configure the plugin of the same name here. Last but not least, here you need to configure the plugin to work with each of the modules (in ), namely the plugin Jandex… Since Quarkus uses CDI, the Jandex plugin adds an index file to each module; the file contains records of all annotations used in this module and links indicating where which annotation is used. As a result, CDI is much easier to handle, and there is much less work to be done later.

Now that the basic structure is ready, you can start building a complete application. To do this, you need to ensure that the main-partition creates the Quarkus executable application. This mechanism is illustrated in any “quick start” example provided in Quarkus.

First, we configure the build to use the Quarkus plugin:


  
    
      io.quarkus
      quarkus-maven-plugin
      
        
          
            build
          
        
      
    
  

Next, let’s add dependencies to each of the application modules, where they will be along with the dependencies quarkus-resteasy and quarkus-jdbc-mysql… In the last dependency, you can replace the database with the one that you like best (considering that later we are going to go the native development path, and therefore we cannot use an embedded database, for example, H2).

Alternatively, you can add a profile so you can build the native application later. To do this, you really need an additional development stand (GraalVM, native-image and XCode if you are using OSX).


  
    native
    
      
        native
      
    
    
      native
    
  

Now if you run mvn package quarkus:dev from the project root, you will have a working Quarkus app! There is not much to see yet, since we have neither controllers nor content yet.

Adding a REST controller

In this exercise, let’s go from the periphery to the core. First, let’s create a REST controller that will return custom data (in this example, this is just the name).

To use the JAX-RS API, a dependency must be added to the infra-web POM:


  io.quarkus
  quarkus-resteasy-jackson

The simplest controller code looks like this:

@Path("/customer")
@Produces(MediaType.APPLICATION_JSON)
public class CustomerResource {
    @GET
    public List list() {
        return getCustomers.getCustomer().stream()
                .map(response -> new JsonCustomer(response.getName()))
                .collect(Collectors.toList());
    }

    public static class JsonCustomer {
        private String name;

        public JsonCustomer(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }

If we run the application now, we can call localhost: 8080 / customer and see Joe in JSON format.

Add a specific case

Next, let’s add a case and implementation for this practical case. IN app-api let’s define the following case:

public interface GetCustomers {
    List getCustomers();

    class Response {
        private String name;

        public Response(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }
}

IN app-impl let’s create the simplest implementation of this interface.

@UseCase
public class GetCustomersImpl implements GetCustomers {
    private CustomerGateway customerGateway;

    public GetCustomersImpl(CustomerGateway customerGateway) {
        this.customerGateway = customerGateway;
    }

    @Override
    public List getCustomers() {
        return Arrays.asList(new Response("Jim"));
    }
}

So that CDI can see the component GetCustomersImpl, you need a custom annotation UseCase as defined below. Also can use standard ApplicationScoped and annotation Transactional, but by creating your own annotation, you get the ability to more logically group your code and detach your implementation code from frameworks such as CDI.

@ApplicationScoped
@Transactional
@Stereotype
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
}

To use CDI annotations, add the following dependencies to the POM file app-impl in addition to dependencies app-api and domain


  jakarta.enterprise
  jakarta.enterprise.cdi-api


  jakarta.transaction
  jakarta.transaction-api

Next, we need to change the REST controller to use it in cases app-api

...
private GetCustomers getCustomers;

public CustomerResource(GetCustomers getCustomers) {
    this.getCustomers = getCustomers;
}

@GET
public List list() {
    return getCustomers.getCustomer().stream()
            .map(response -> new JsonCustomer(response.getName()))
            .collect(Collectors.toList());
}
...

If you now run the application and call localhost: 8080 / customer you will see Jim in JSON format.

Definition and implementation of the domain

Next, let’s take a look at the domain. Here is the essence domain quite simple, it consists of Customer and the gateway interface through which we will receive consumers.

public class Customer {
	private String name;

	public Customer(String name) {
		this.name = name;
	}

	public String getName() {
		return name;
	}
}
public interface CustomerGateway {
	List getAllCustomers();
}

We also need to provide an implementation of the gateway before we can start using it. We provide such an interface in infra-persistence

For this implementation, we will use the JPA support available in Quarkus, and we will also use the framework Panache, which will make our life a little easier. In addition to domain, we will have to add to infra-persistence the following dependency:


  io.quarkus
  quarkus-hibernate-orm-panache

First, we define the JPA entity corresponding to the consumer.

@Entity
public class CustomerJpa {
	@Id
	@GeneratedValue
	private Long id;
	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

When working with Panache, you can choose one of two options: either your entities will inherit PanacheEntity, or you will use the repository / DAO pattern. I’m not a fan of the ActiveRecord pattern, so I’ll stop at the repository myself, but what you will work with is up to you.

@ApplicationScoped
public class CustomerRepository implements PanacheRepository {
}

Now that we have our JPA entity and repository, we can implement the gateway Customer

@ApplicationScoped
public class CustomerGatewayImpl implements CustomerGateway {
	private CustomerRepository customerRepository;

	@Inject
	public CustomerGatewayImpl(CustomerRepository customerRepository) {
		this.customerRepository = customerRepository;
	}

	@Override
	public List getAllCustomers() {
		return customerRepository.findAll().stream()
				.map(c -> new Customer(c.getName()))
				.collect(Collectors.toList());
	}
}

Now we can change the code in the implementation of our case, so that it uses the gateway.

...
private CustomerGateway customerGateway;

@Inject
public GetCustomersImpl(CustomerGateway customerGateway) {
    this.customerGateway = customerGateway;
}

@Override
public List getCustomer() {
    return customerGateway.getAllCustomers().stream()
            .map(customer -> new GetCustomers.Response(customer.getName()))
            .collect(Collectors.toList());
}
...

We cannot start our application yet, because the Quarkus application still needs to be configured with the required persistence parameters. IN src/main/resources/application.properties in module main-partition add the following parameters.

quarkus.datasource.url=jdbc:mysql://localhost/test
quarkus.datasource.driver=com.mysql.cj.jdbc.Driver
quarkus.hibernate-orm.dialect=org.hibernate.dialect.MySQL8Dialect
quarkus.datasource.username=root
quarkus.datasource.password=root
quarkus.datasource.max-size=8
quarkus.datasource.min-size=2
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.sql-load-script=import.sql

To view the initial data, we will also add the file import.sql in the same directory from which the data is added.

insert into CustomerJpa(id, name) values(1, 'Joe');
insert into CustomerJpa(id, name) values(2, 'Jim');

If you now run the application and call localhost: 8080 / customer you will see Joe and Jim in JSON format. So, we have a complete application, from REST to the database.

Native option

If you want to build a native application, then you need to do this using the command mvn package -Pnative… This can take about a couple of minutes, depending on what your development stand is. Quarkus is quite fast at startup and without native support, starts in 2-3 seconds, but when compiled into a native executable using GraalVM, the corresponding time is reduced to less than 100 milliseconds. For a Java application, that’s blazing fast.

Testing

You can test the Quarkus application using the corresponding Quarkus test framework. If you annotate the test @QuarkusTestthen JUnit will first launch the Quarkus context and then execute the test. Test the whole application in main-partition will look something like this:

@QuarkusTest
public class CustomerResourceTest {
	@Test
	public void testList() {
		given()
				.when().get("/customer")
				.then()
				.statusCode(200)
				.body("$.size()", is(2),
						"name", containsInAnyOrder("Joe", "Jim"));
	}
}

Conclusion

In many ways, Quarkus is a fierce competitor to Spring Boot. In my opinion, some things in Quarkus are even better solved. Even though app-impl has a framework dependency, it’s just a dependency for annotations (in the case of Spring, when we add spring-context, To obtain @Component, we add a lot of Spring core dependencies). If you don’t like this, you can also add the Java file to the main section using the annotation @Produces from CDI and the component that creates there; in this case, you don’t need any additional dependencies in app-impl… But for some reason addiction jakarta.enterprise.cdi-api I want to see there less than addiction spring-context

Quarkus is fast, really fast. It is faster than Spring Boot with this type of application. Since, according to the Clean Architecture, most (if not all) of the framework’s dependencies should reside on the outside of the application, the choice between Quarkus and Spring Boot becomes obvious. In this regard, the advantage of Quarkus is that it was immediately created with GraalVM support in mind, and therefore, at the cost of minimal effort, it allows you to turn the application into a native one. Spring Boot is still lagging behind Quarkus in this regard, but I have no doubt that it will catch up soon.

However, experimenting with Quarkus also helped me realize the many misfortunes awaiting those who try to use Quarkus with the classic Jakarta EE application servers. While there isn’t much that can be done with Quarkus yet, its code generator supports a variety of technologies that are not yet easy to use in the context of Jakarta EE with a traditional application server. Quarkus covers all the basics people familiar with Jakarta EE will need, and development on it is much smoother. It will be interesting to see how the Java ecosystem can handle this kind of competition.

All the code for this project is posted on Github

Similar Posts

Leave a Reply