Practical Guide: Migrating from Spring to Quarkus with CockroachDB — A Hands-on Experience

Zuda Pradana Putra
8 min readFeb 29, 2024

Quarkus is a Java framework designed specifically for GraalVM and HotSpot, built from the best Java libraries. The goal is to make Java the leading platform in Kubernetes and serverless application development. For those of you who are already familiar with Spring Boot, there are several reasons why you should try Quarkus:

Fast Startup and Low Memory Usage

Quarkus optimizes Java for fast startup and low memory usage, which is perfect for cloud applications and serverless functions that require short startup times and resource efficiency.

Live Development Mode

Quarkus features a live development mode that allows you to make changes to the code and instantly see the results, without the need to restart or re-deploy the application.

Extensive Support for Standard Libraries

Quarkus supports various standard libraries commonly used in the Java ecosystem such as Hibernate, Apache Camel, Eclipse MicroProfile and many more.

Familiar Development Experience

Although Quarkus offers many new features, it is designed to provide a familiar development experience for those who are already familiar with the Java ecosystem and Spring Boot.

In this article, I will share my first experience trying Quarkus through building a simple food ordering application. We will focus on basic CRUD operations using Panache, Quarkus’ built-in ORM, and some other libraries such as SmallRye, Lombok, PostgreSQL JDBC, RESTEasy, and RESTEasy Jackson. I hope this can provide an initial overview of using Quarkus in modern Java application development.

What about the database we will use? CockroachDB is a good choice for modern applications for several reasons. First, it offers high availability and fault tolerance with automatic data replication and geographic deployment. Second, CockroachDB supports ACID transactions, ensuring data consistency. Third, it can scale horizontally easily to handle large workloads. In addition, CockroachDB uses the PostgreSQL connection protocol. This means you can use existing PostgreSQL drivers and tools to interact with CockroachDB

Schema Database Food Order

I will not explain long-windedly, let’s start the quarkus project right away. You can use your favorite IDE, but in this hand on I will use Intellij IDEA and generate the project directly, but you can also visit this link.

1. Setup App.properties, Create DB and Entity

Let’s start with the installation of CockroachDB. For Windows users who want to run CockroachDB locally, I recommend following the installation guide in the following article. Once the installation is complete, you can run CockroachDB using the commands that we will discuss next.

Please provide the commands you would like to run after installing CockroachDB, and I will help frame the explanation.

cockroach start-single-node --insecure `
--store=node1 `
--listen-addr=localhost:26257 `
--http-addr=localhost:8081


//if not error, try command below to access db
sql --insecure --host=localhost:26257

Maybe you will experience a very annoying error, if I’m not mistaken about the timezone? first do the installation of the Golang language, yes Golang ahaha. after that you can create a variable path to access the time zone.

Path Timezone

you can follow my way, it worked for me and finally I can access cockroach db. After that you can create a new db with whatever name ‘CREATE DATABASE your_db;’. I assume the db problem is solved, let’s create a connection in the application.properties project of quarkus.

#swagger
quarkus.swagger-ui.always-include=true

# Driver JDBC
quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.driver=org.postgresql.Driver
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:26257/mydb?sslmode=disable
quarkus.datasource.username=root
quarkus.hibernate-orm.database.generation=update
quarkus.hibernate-orm.dialect=org.hibernate.dialect.PostgreSQLDialect

Once the database connection has been successfully applied, the next step is to create entities. In this context, entities are representations of the tables in the database that we will use in our application. By using Hibernate, we can allow the framework to manage the automatic creation and update of tables based on the entities we define. To create an entity, we simply create a regular class and add the @Entity annotation above the class declaration. The @Entity annotation tells Hibernate that the class should be managed as an entity and the quarkus will be scanned when the project is built. Use the @Data annotation to save you time in creating getters and setters with the help of Lombok dependencies.

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "order_detail")
public class OrderDetail {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "order_details_id")
public Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
Order order;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "food_id")
Food food;

@Column(name = "qty")
public int qty;
}

In the above code, we define the OrderDetail entity that represents the order details in our application. This entity is decorated with @Entity, @Data, @AllArgsConstructor, and @NoArgsConstructor annotations that provide various functions such as the creation of getters and setters, constructors, and more. This entity has four attributes: id, order, food, and qty. The id attribute is an automatically generated primary key. The order and food attributes are relations to other entities and are marked with the @ManyToOne annotation, indicating that each order detail is related to one order and one food. The qty attribute represents the number of meals in that order. All these attributes are mapped to specific columns in the database table through the @Column annotation.

Other tables you can customize again as I have explained, for this practice I only explain the crud of the Customer table. But this mini project output is how the cashier inputs the food ordered by the customer and provides responses such as total payment, total price, type of payment, total change, that’s why in this simple project I also modeled the relationship of the database as in the schema image at the beginning.

2. Repository and Service

The second stage in this process is the creation of a repository for each main entity and the creation of services as the business logic layer. Here, I use the repository pattern. The repository pattern is a design pattern that separates data access logic from business logic. In this context, the classes or interfaces will implement the Object-Relational Mapping (ORM) that we have installed before, namely Hibernate Panache (similar to the use of JPA in Spring).

The service layer will inject this repository class or interface to access or communicate directly with the database. Thus, the service layer acts as an intermediary between the application and the database, allowing us to keep the business logic separate from the data access logic. This helps keep our code tidy and maintainable.

@ApplicationScoped
public class CustomerRepositories implements PanacheRepository<Customer> {

}

Above is the class that I created in the package repository. Actually you can create a special filter here for parameters maybe? but because I will make it as simple as possible so I leave it empty and use this repo injection in the service layer only. If you are used to using springboot, the annotation might be @Repository

@ApplicationScoped
@Slf4j
public class CustomerServices {
@Inject
CustomerRepositories repositories;

@Inject
ModelMapperConf mapperConf;


public List<Customer> getAllCustomers(){
log.info("get all customer data");
return repositories.listAll();
}

public CustomerDataRes getCustomerById(Long id){
var customer = repositories.findById(id);
if(customer == null){
log.error("Id Not Found");
throw new RuntimeException("Id Not Found");
}
log.info("Retrieve id {}", id);
return mapperConf.toCustomerDTO(customer);
}

@Transactional
public Customer createCustomer(CustomerDataReq customerDataReq){
Customer customer = Customer.builder()
.name(customerDataReq.getCustomerName())
.email(customerDataReq.getCustomerEmail())
.phoneNumber(customerDataReq.getCustomerPhone())
.build();
repositories.persist(customer);
log.info("create customer");
return customer;
}


@Transactional
public Customer updateCustomer(Long id, CustomerDataReq customerDataReq){
var updateCustomer = repositories.findById(id);
updateCustomer.setName(customerDataReq.getCustomerName());
updateCustomer.setEmail(customerDataReq.getCustomerEmail());
updateCustomer.setPhoneNumber(customerDataReq.getCustomerPhone());

log.info("Customer with id {} is edited", id);
repositories.persist(updateCustomer);
return updateCustomer;
}

public void deleteCustomer(Long id){
var customerId = getCustomerById(id).getId();
repositories.deleteById(customerId);
log.info("ID {} deleted", id);
}
}

The code above is a service in the Quarkus application that manages customer-related operations. The @Slf4j annotation is used for logging, allowing us to record important information during program execution. The @Transactional annotation guarantees that annotated methods will be executed in the context of a transaction, ensuring data integrity. This service provides various functions such as getting all customers, getting customers by ID, creating, updating, and deleting customers, all by interacting with the database through an injected repository. In Spring Boot, the @Service annotation is used to mark a class as a service. This helps Spring know that the class should be managed as a bean in the context of a Spring application.On the other hand, Quarkus uses the @ApplicationScoped annotation for the same purpose. This annotation is derived from CDI (Contexts and Dependency Injection) which is a Java specification for dependency injection and object lifecycle management.

3. Controller or Resource

In Quarkus, the controller layer (a term in springboot) is usually referred to as the resource layer. This layer is responsible for handling HTTP requests and directing them to the appropriate service. Each class in the resource layer is usually defined with the @Path annotation and the methods within that class are defined with annotations such as @GET, @POST, @PUT, and @DELETE that correspond to the HTTP methods they handle. This resource layer serves as the entry point for HTTP requests and as the link between those requests and the application’s business logic.

@Path("/customer")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class CustomerResources {
@Inject
CustomerServices services;

@GET
public RestResponse<List<Customer>> getAll(){
var response = services.getAllCustomers();
return RestResponse.ResponseBuilder.ok(response).build();
}

@GET
@Path("/{id}")
public RestResponse<CustomerDataRes> getCustomerById(@PathParam("id") Long id){
var response = services.getCustomerById(id);
return RestResponse.ResponseBuilder.ok(response).build();
}

@POST
public RestResponse<Customer> createCustomer(CustomerDataReq customerDataReq){
var response = services.createCustomer(customerDataReq);
return RestResponse.ResponseBuilder.ok(response).build();
}

@PUT
@Path("/{id}")
public RestResponse<Customer> updateCustomer(@PathParam("id") Long id, CustomerDataReq customerDataReq){
var response = services.updateCustomer(id, customerDataReq);
return RestResponse.ResponseBuilder.ok(response).build();
}

@DELETE
@Path("/{id}")
public RestResponse<Void> deleteCustomer(@PathParam("id") Long id){
services.deleteCustomer(id);
return RestResponse.ok();
}
}

The above code is a resource class in Quarkus that exposes customer-related operations as a web service. The @Path, @Produces, and @Consumes annotations are used to define the URL path, output data format, and input. The @Inject annotation is used to inject a CustomerServices instance into this class. The @GET, @POST, @PUT, and @DELETE annotations are used to define the corresponding HTTP methods.

In Spring Boot, we usually use the @RestController annotation to define the class as a REST controller. This controller will then handle the web request and return a response. However, in Quarkus, we use the @Path annotation for the same purpose. Although different in syntax, both serve the same purpose which is to expose application operations as web services. To test the API endpoints we have created, we can utilize the Swagger UI available at http://localhost:8080/q/swagger-ui/. It provides an easy-to-use interface to interact with our API and validate its functionality.

That concludes this hands-on article. Through this process, we have seen how Quarkus and CockroachDB can be used together to build efficient and scalable modern Java applications. By utilizing features like Panache, SmallRye, and Lombok, we can speed up the development process and ensure that our applications are ready for the challenges offered by cloud and serverless environments. Thank you for following this journey with me. I hope you found this material useful and informative. If you have any questions or comments, please feel free to leave them below. Good luck and happy learning! Github

--

--