The Quarkus framework: how clean architecture is implemented in it

Hello, Habr!



As we continue our exploration of new Java frameworks and your interest in the Spring Boot book , we're looking at the new Quarkus framework for Java. You can 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 adhere to a clean architecture using Quarkus .



Quarkus is quickly gaining the status of a framework that cannot be avoided. Therefore, I decided to go over 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-apiand 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-partitionthat will serve as a 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 dependencies that you use. You will also need to configure standard plugins for maven projects in your plugin management tool, such as the surefire plugin . 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 <build><plugins>...</plugins> </build>), namely the Jandex plugin... 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, the CDI is much easier to handle, with 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 make sure 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:



<build>
  <plugins>
    <plugin>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-maven-plugin</artifactId>
      <executions>
        <execution>
          <goals>
            <goal>build</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>


Next, let's add dependencies to each of the application modules, where they will be along with dependencies quarkus-resteasyand 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 follow 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-imageand XCode if you're using OSX).



<profiles>
  <profile>
    <id>native</id>
    <activation>
      <property>
        <name>native</name>
      </property>
    </activation>
    <properties>
      <quarkus.package.type>native</quarkus.package.type>
    </properties>
  </profile>
</profiles>


Now, if you run mvn package quarkus:devfrom the project root, you have a working Quarkus app! There isn't much to see yet, since we don't have any controllers or 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 user 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:



<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>


The simplest controller code looks like this:



@Path("/customer")
@Produces(MediaType.APPLICATION_JSON)
public class CustomerResource {
    @GET
    public List<JsonCustomer> 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 it Joein JSON format.



Add a specific case



Next, let's add a case and implementation for this practical case. Let's app-apidefine the following case:



public interface GetCustomers {
    List<Response> getCustomers();

    class Response {
        private String name;

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

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


The app-implcreate a simple implementation of this interface.



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

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

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


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



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


To use CDI annotations, you must add the following dependencies to the POM file app-implin addition to the app-apiand dependencies domain.



<dependency>
  <groupId>jakarta.enterprise</groupId>
  <artifactId>jakarta.enterprise.cdi-api</artifactId>
</dependency>
<dependency>
  <groupId>jakarta.transaction</groupId>
  <artifactId>jakarta.transaction-api</artifactId>
</dependency>


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



...
private GetCustomers getCustomers;

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

@GET
public List<JsonCustomer> 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 it Jimin JSON format.



Definition and implementation of the domain



Next, we will focus on the domain. The essence here is domainquite simple, it consists of Customera 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<Customer> 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 also use the Panache framework , which will make our life a little easier. In addition to domain, we will have to add the infra-persistencefollowing dependency to:



<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>


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<CustomerJpa> {
}


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<Customer> getAllCustomers() {
		return customerRepository.findAll().stream()
				.map(c -> new Customer(c.getName()))
				.collect(Collectors.toList());
	}
}


Now you 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<Response> 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.propertiesthe module, main-partitionadd 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 original data, we will also add the file import.sqlto 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 Joeit Jimin JSON format as well. 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 @QuarkusTest, JUnit will first launch the Quarkus context and then execute the test. A test of the entire application in main-partitionwill 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-contextto get @Component, we add a lot of Spring core dependencies). If you don't like this, you can also add a Java file to the main section, using the annotation @Producesfrom CDI and creating the component there; in this case, you don't need any additional dependencies in app-impl. But for some reason, jakarta.enterprise.cdi-apiI want to see addiction 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 respect, 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 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 that 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 .



All Articles