Building a Microservices-Based Expense App with Java & Quarkus: A Detailed Walkthrough
In this blog, we will explore how to build a basic expense management application using Java with Quarkus. With Quarkus and Java, you can rapidly build microservices and expose them as RESTful APIs. We’ll organize the app into three key components:
- Model: The Expense class (for defining the structure of the data).
- Service Layer: The ExpenseRepository (for CRUD operations).
- REST API Layer: The ExpenseResource (for creating RESTful API endpoints).
Component 1: The Expenses
Class (Data Model)
This class represents the structure of the data for each expense. It stores information like the name of the expense, the amount, payment method, and creation date.
package com.harsh.tech;
import jakarta.json.bind.annotation.JsonbCreator;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
public class Expenses {
enum PaymentMethod{
CASH, CREDIT_CARD, UPI
}
private String name;
private BigDecimal amount;
private LocalDateTime creationDate;
private PaymentMethod paymentMethod;
private UUID uuid;
public Expenses(String name, BigDecimal amount, PaymentMethod paymentMethod) {
this.name = name;
this.amount = amount;
this.creationDate = LocalDateTime.now();
this.paymentMethod = paymentMethod;
this.uuid = UUID.randomUUID();
}
@JsonbCreator
public static Expenses of(String name, BigDecimal amount, PaymentMethod paymentMethod) {
return new Expenses(name, amount, paymentMethod);
}
public UUID getUUID(){ return uuid; }
public void setUUID(UUID myuuid){ this.uuid = myuuid; }
public String getName(){ return name; }
public void setName(String myname){ this.name = myname; }
public LocalDateTime getCreationDate(){ return creationDate; }
public void setCreationDate(LocalDateTime myCreationDate){ this.creationDate = myCreationDate; }
public BigDecimal getAmount(){ return amount; }
public void setAmount(BigDecimal myamount){ this.amount = myamount; }
public PaymentMethod getPaymentMethod(){ return paymentMethod; }
public void setPaymentMethod(PaymentMethod mypayment){ this.paymentMethod = mypayment; }
}
Explanation of Code:
- Enum
PaymentMethod
: Defines the different ways an expense can be paid (cash, credit card, or UPI). - Class Fields:
name
,amount
,creationDate
,paymentMethod
, anduuid
are the attributes that define each expense. UUID
: A unique identifier for each expense, generated automatically when the expense object is created.creationDate
: The time the expense is recorded, set to the current time.name
andamount
: Basic details about the expense.- Constructor: Initializes the
Expenses
object by taking thename
,amount
, andpaymentMethod
as inputs, sets the current time ascreationDate
, and generates aUUID
. @JsonbCreator
: This annotation tells Quarkus to create an instance of theExpenses
class using theof()
method when deserializing JSON.- Getters and Setters: Provide access and modification to the private fields of the class.
Component 2: The ExpensesServices
Class (Service Layer)
This class handles the business logic of our application. It stores expenses in a synchronized set and provides basic CRUD operations.
package com.harsh.tech;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.*;
@ApplicationScoped
public class ExpensesServices {
Set<Expenses> MyExpenses = Collections.newSetFromMap(Collections.synchronizedMap(new HashMap<>()));
// CREATE
public Expenses create(Expenses expenses){
MyExpenses.add(expenses);
return expenses;
}
// READ
public Set<Expenses> list(){
return MyExpenses;
}
// UPDATE
public void update(Expenses expenses){
delete(expenses.getUUID());
create(expenses);
}
// DELETE
public boolean delete(UUID uuid){
return MyExpenses.removeIf(expenses -> expenses.getUUID().equals(uuid) );
}
}
Explanation of Code:
@ApplicationScoped
: This annotation makes the service accessible across the application lifecycle. The same instance is reused in the app, ensuring data consistency.MyExpenses
: A thread-safe set that holds all expenses. It is synchronized to ensure safe operations when accessed by multiple threads.
CRUD Methods:
- Create: Adds an
Expenses
object toMyExpenses
and returns it. - List: Returns all the expenses stored in
MyExpenses
. - Update: Deletes an expense by its UUID and then adds the updated expense. This simulates an update by first removing the old entry and then adding the modified one.
- Delete: Removes an expense if its UUID matches the provided one.
Component 3: The ExpenseResources
Class (REST API)
This class exposes REST API endpoints, making the service accessible to clients via HTTP.
package com.harsh.tech;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import java.util.Set;
import java.util.UUID;
@Path("/expenses")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ExpenseResources {
@Inject
ExpensesServices expensesServices;
@POST
public Expenses myCreate(Expenses expense){
return expensesServices.create(expense);
}
@GET
public Set<Expenses> myList(){
return expensesServices.list();
}
@PUT
public void myUpdate(Expenses expense){
expensesServices.update(expense);
}
@DELETE
@Path("{uuid}")
public boolean myDelete(@PathParam("uuid") UUID uuid){
return expensesServices.delete(uuid);
}
}
Explanation of Code:
@Path("/expenses")
: Sets the base URL for the expense-related APIs. All routes inside this class will start with/expenses
.@Inject
: Injects an instance ofExpensesServices
so that the resource class can access its methods.@POST
: This annotation exposes a POST method at/expenses
, allowing clients to create a new expense.@GET
: Retrieves all expenses from thelist()
method of the service class and is mapped to the/expenses
GET request.@PUT
: Allows updating an existing expense by calling theupdate()
method.@DELETE @Path("{uuid}")
: Maps the DELETE request with a path parameter (uuid
) to delete a specific expense by its UUID.
Testing the API Using Postman
Once the application is set up, you can test it with Postman to perform all CRUD operations:
- Run the Application:
.\mvnw compile quarkus:dev
- POST /expenses: Create an expense by providing JSON data like:
{
"name": "Movie",
"amount": 200,
"paymentMethod": "CASH"
}
- GET /expenses: Retrieve the list of all expenses.
You can also verify the GET method via browser.
- PUT /expenses: Update an existing expense by sending a modified expense object.
Here we also need to send the UUID of the expense that we need to update along with our data. After sending, let’s verify using GET method.
- DELETE /expenses/{uuid}: Delete an expense by specifying its UUID.
Since it return the boolean, true indicates that the delete is successful. Let’s verify again using the GET method.
Conclusion
This tutorial demonstrates how to build and organize a simple expense management app using Quarkus. By breaking the app into separate components — model, service, and resource — you maintain a clean architecture and make the app easier to manage and scale. With Quarkus and Java, you can rapidly build microservices and expose them as RESTful APIs. Quarkus also provides powerful testing frameworks and integration tools to help you build, test, and deploy microservices efficiently.