Build Smarter Approval Workflows with Quarkus and Drools
Create a dynamic expense approval engine in Java with hot-reloadable business rules and zero downtime updates.
Enterprises rely on approval workflows to manage expenses, procurement, and other compliance-heavy processes. These workflows must adapt to evolving policies without requiring downtime or redeployments. Hardcoding business logic into Java classes makes changes costly and slow. Instead, externalizing rules to a business rules engine like Drools provides flexibility. Combined with Quarkus, you can create a cloud-native approval system that supports hot reloading of rules for rapid iteration and deployment.
In this tutorial, you will build a dynamic expense approval system with Quarkus and Drools. The application will accept expense reports, apply configurable business rules, and return an approval chain. You will see how to modify rules on the fly without restarting the application.
Setting Up the Project
Open a terminal and create a new Quarkus project with REST Jackson for the API and Drools for rule evaluation.
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=approval-workflow \
-DclassName="org.acme.ApprovalResource" \
-Dpath="/approvals" \
-Dextensions="rest-jackson,drools-quarkus"
cd approval-workflow
This scaffolds a new Quarkus project with a REST endpoint template and Drools support. Open the project in your IDE.
Defining the Domain Model
Drools works with Java objects called facts. Define a simple ExpenseReport
POJO representing an expense submission.
Create src/main/java/org/acme/ExpenseReport.java
:
package org.acme;
import java.util.ArrayList;
import java.util.List;
public class ExpenseReport {
private double amount;
private String department;
private String submittedBy;
private List<String> approvalChain = new ArrayList<>();
public double getAmount() { return amount; }
public void setAmount(double amount) { this.amount = amount; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public String getSubmittedBy() { return submittedBy; }
public void setSubmittedBy(String submittedBy) { this.submittedBy = submittedBy; }
public List<String> getApprovalChain() { return approvalChain; }
public void setApprovalChain(List<String> approvalChain) { this.approvalChain = approvalChain; }
public void addApprovalStep(String approver) {
this.approvalChain.add(approver);
}
}
This class holds the expense data and keeps track of which approvers must review it.
Writing the Business Rules
Rules are defined in Drools Rule Language (DRL) files. Create a new directory src/main/resources/org/acme/rules
and add approval-rules.drl
:
package org.acme.rules;
import org.acme.ExpenseReport;
// Auto-approve small expenses
rule "Auto-Approve Low Amount"
when
$report: ExpenseReport(amount < 50)
then
$report.addApprovalStep("System (Auto-Approved)");
end
// Manager approval for medium expenses
rule "Manager Approval for Medium Amount"
when
$report: ExpenseReport(amount >= 50 && amount < 500)
then
$report.addApprovalStep("Manager");
end
// Senior Manager approval for large expenses
rule "Senior Manager Approval for High Amount"
when
$report: ExpenseReport(amount >= 500)
then
$report.addApprovalStep("Manager");
$report.addApprovalStep("Senior Manager");
end
// Sales department always requires Finance approval
rule "Finance Approval for Sales Department"
salience 10
when
$report: ExpenseReport(department == "Sales")
then
$report.addApprovalStep("Finance Department");
end
Each rule has conditions (when
) and actions (then
). The salience
keyword increases the priority of a rule to ensure it executes first.
Creating the REST API
Modify the generated ApprovalResource.java
to process incoming expense reports through Drools.
package org.acme;
import org.kie.api.runtime.KieSession;
import org.kie.api.runtime.KieRuntimeBuilder;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/approvals")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ApprovalResource {
@Inject
KieRuntimeBuilder runtimeBuilder;
@POST
@Path("/calculate")
public ExpenseReport calculateApproval(ExpenseReport report) {
KieSession ksession = runtimeBuilder.newKieSession();
ksession.insert(report);
ksession.fireAllRules();
ksession.dispose();
return report;
}
}
Each request creates a new KieSession
, inserts the expense report, fires all applicable rules, and returns the modified report.
Start the application and Test the Rules
Start the app in development mode:
./mvnw quarkus:dev
When you edit a DRL file, Quarkus recompiles the rules instantly without restarting the app.
Testing the Workflow
Send sample requests using curl
:
Small Expense
curl -X POST -H "Content-Type: application/json" \
-d '{"amount": 45.0, "department": "IT", "submittedBy": "dev1"}' \
http://localhost:8080/approvals/calculate
Expected output:
{
"amount": 45.0,
"department": "IT",
"submittedBy": "dev1",
"approvalChain": [
"System (Auto-Approved)"
]
}
Large Expense
curl -X POST -H "Content-Type: application/json" \
-d '{"amount": 750.0, "department": "HR", "submittedBy": "hr1"}' \
http://localhost:8080/approvals/calculate
Expected output:
{
"amount": 750.0,
"department": "HR",
"submittedBy": "hr1",
"approvalChain": [
"Manager",
"Senior Manager"
]
}
Sales Department Expense
curl -X POST -H "Content-Type: application/json" \
-d '{"amount": 120.0, "department": "Sales", "submittedBy": "sales1"}' \
http://localhost:8080/approvals/calculate
Expected output:
{
"amount": 120.0,
"department": "Sales",
"submittedBy": "sales1",
"approvalChain": [
"Finance Department",
"Manager"
]
}
Updating Rules Without Restarting
While the app runs in dev mode, change the Auto-Approve Low Amount
rule to approve amounts under 100:
rule "Auto-Approve Low Amount"
when
$report: ExpenseReport(amount < 100)
then
$report.addApprovalStep("System (Auto-Approved)");
end
Save the file. Quarkus recompiles automatically. Now test with a $90 expense:
curl -X POST -H "Content-Type: application/json" \
-d '{"amount": 90.0, "department": "IT", "submittedBy": "dev2"}' \
http://localhost:8080/approvals/calculate
The response should now auto-approve the expense.
Where to Go Next
You have built a dynamic, rule-driven approval system using Quarkus and Drools. The rules are externalized, making them easy to modify without redeployments. From here you could:
Load rules dynamically from a database or external repository.
Build a web interface to manage and test rules.
Add complex facts like employee roles, budgets, or regional policies.
Combine Drools with event-driven architectures using Quarkus Messaging.
For more details, explore the Quarkus Drools Extension Guide and Drools documentation.