The Supermarket Sleuth: Mining Hidden Shopping Patterns with Java and Quarkus
Learn how to uncover real-world customer insights using Quarkus, Panache, PostgreSQL, and the Apriori algorithm in a hands-on Java project.
Picture this: you’re not just coding, you’re investigating. The owner of a quirky local grocery store, The Quarky Cart, has a mystery. They’re swimming in transaction data, but it’s just rows and rows of receipts. They want to know:
Should they bundle bread and milk? Do people who buy diapers also pick up beer?
This is where you step in. Your mission: build a Java application that finds these hidden shopping patterns using association rule learning.
We’ll use:
Quarkus for a lean, fast Java backend.
Hibernate ORM with Panache to keep data access simple.
PostgreSQL to hold the transaction receipts.
The Apriori Algorithm as our detective’s magnifying glass.
Let’s get started.
Association Rules in Plain Language
Think about browsing thousands of shopping baskets. Association rule learning pulls out rules like:
If a customer buys {Item A}, they’re also likely to buy {Item B}.
To separate real insights from random noise, we rely on three metrics:
Support: How often items appear together in all transactions.
Example: 10 out of 100 baskets have both milk and bread → support = 10%.
Confidence: How reliable a rule is.
Example: Of all diaper purchases, 80% also include beer → confidence = 80%.
Lift: How much more often items appear together than by chance.
Example: If beer appears in 20% of all baskets, but 80% of diaper baskets, lift = 4. Strong signal.
The Apriori algorithm works by finding frequent item sets (above a support threshold) and then generating rules with high confidence.
Setting Up Your Quarkus Project
Bootstrap your project with Quarkus CLI or code.quarkus.io and add:
quarkus-rest-jackson
quarkus-hibernate-orm-panache
quarkus-jdbc-postgresql
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=quarky-cart \
-Dextensions="rest-jackson,hibernate-orm-panache,jdbc-postgresql"
cd quarky-cart
Configure application.properties
:
quarkus.datasource.db-kind=postgresql
# Rebuild schema and load seed data
quarkus.hibernate-orm.schema-management.strategy=drop-and-create
As usual, you can find the complete source code in my Github repository.
Creating a Dataset
For a first run, we’ll seed the database with 25 transactions.
Create the Transaction entity:
package com.example;
import java.util.Set;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
@Entity
public class Transaction extends PanacheEntity {
// A Set is better than a List here since item order doesn't matter
// and items are unique within a transaction.
@ElementCollection(fetch = FetchType.EAGER)
public Set<String> items;
}
import.sql (sample extract). Get the full set from my Github repository.
INSERT INTO Transaction(id) VALUES (1), (2), (3);
INSERT INTO Transaction_items(Transaction_id, items) VALUES
(1, 'whole milk'), (1, 'rolls/buns'), (1, 'yogurt'),
(2, 'diapers'), (2, 'canned beer'), (2, 'napkins'),
(3, 'citrus fruit'), (3, 'pastry'), (3, 'soda');
Hibernate will map @ElementCollection
to a join table called Transaction_items
.
Implementing the Apriori Logic
We’ll keep things simple: analyze pairs of items and build rules.
Create the AssociationRuleService.java:
package org.acme;
package com.example;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AssociationRuleService {
// A simple record to hold our final rule and its metrics
public record AssociationRule(Set<String> antecedent, Set<String> consequent, double confidence, double lift,
double support) {
}
public List<AssociationRule> findAssociationRules(double minSupport, double minConfidence) {
List<Set<String>> transactions = Transaction.<Transaction>listAll().stream()
.map(t -> t.items)
.collect(Collectors.toList());
int numTransactions = transactions.size();
Map<Set<String>, Integer> itemsetSupportCount = new HashMap<>();
Map<String, Integer> singleItemSupportCount = new HashMap<>();
// Calculate support counts for single items and pairs
for (Set<String> transaction : transactions) {
List<String> items = new ArrayList<>(transaction);
for (int i = 0; i < items.size(); i++) {
String itemA = items.get(i);
singleItemSupportCount.put(itemA, singleItemSupportCount.getOrDefault(itemA, 0) + 1);
for (int j = i + 1; j < items.size(); j++) {
String itemB = items.get(j);
Set<String> pair = new TreeSet<>(Arrays.asList(itemA, itemB)); // Use TreeSet for consistent
// ordering
itemsetSupportCount.put(pair, itemsetSupportCount.getOrDefault(pair, 0) + 1);
}
}
}
List<AssociationRule> allRules = new ArrayList<>();
// Generate rules from frequent itemsets
for (Map.Entry<Set<String>, Integer> entry : itemsetSupportCount.entrySet()) {
Set<String> itemset = entry.getKey();
int itemsetCount = entry.getValue();
double itemsetSupport = (double) itemsetCount / numTransactions;
if (itemsetSupport >= minSupport) {
// For each item in the pair, create a rule
for (String item : itemset) {
Set<String> antecedent = Set.of(item);
Set<String> consequent = new HashSet<>(itemset);
consequent.remove(item);
int antecedentCount = singleItemSupportCount.get(item);
double confidence = (double) itemsetCount / antecedentCount;
String consequentItem = consequent.iterator().next();
int consequentCount = singleItemSupportCount.get(consequentItem);
double consequentSupport = (double) consequentCount / numTransactions;
double lift = confidence / consequentSupport;
if (confidence >= minConfidence) {
allRules.add(new AssociationRule(antecedent, consequent, confidence, lift, itemsetSupport));
}
}
}
}
allRules.sort(Comparator.comparing(AssociationRule::confidence).reversed());
return allRules;
}
}
Expose Results via REST
Give the store owner a way to trigger analysis.
Create the RuleResource.java:
package com.example;
import java.util.List;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
@Path("/rules")
public class RuleResource {
@Inject
AssociationRuleService ruleService;
@GET
@Path("/discover")
public List<AssociationRuleService.AssociationRule> discoverRules(
@QueryParam("support") Double support,
@QueryParam("confidence") Double confidence) {
// Set reasonable defaults if parameters are not provided
double minSupport = (support != null) ? support : 0.1; // Default: itemset must appear in at least 10% of
// transactions
double minConfidence = (confidence != null) ? confidence : 0.5; // Default: rule must be correct at least 50% of
// the time
return ruleService.findAssociationRules(minSupport, minConfidence);
}
}
Run and Detect Patterns
Start Quarkus:
./mvnw quarkus:dev
Query for rules with 15% support and 60% confidence:
curl -H "Accept: application/json" "http://localhost:8080/rules/discover?support=0.15&confidence=0.6"
Sample output:
[
{
"antecedent": [
"canned beer"
],
"consequent": [
"diapers"
],
"confidence": 1.0,
"lift": 4.166666666666667,
"support": 0.2
},
{
"antecedent": [
"diapers"
],
"consequent": [
"canned beer"
],
"confidence": 0.8333333333333334,
"lift": 4.166666666666667,
"support": 0.2
},
{
"antecedent": [
"rolls/buns"
],
"consequent": [
"whole milk"
],
"confidence": 0.6666666666666666,
"lift": 2.083333333333333,
"support": 0.16
}
]
The "Diapers & Beer" Connection
What the data says:
{diapers} → {canned beer}: When shoppers buy diapers, there's an 83% chance they'll also buy canned beer.
{canned beer} → {diapers}: Even more impressively, every single time a customer bought canned beer, they also bought diapers. This is a 100% correlation in your data!
Lift > 4: This isn't a coincidence. This combination happens over 4 times more often than you'd expect by random chance.
Support = 0.2: This pattern is common, appearing in 20% of all transactions analyzed.
Actionable Insight: This is your strongest and most interesting pattern. It’s likely you have parents making a quick stop for essentials. You should place a display of popular canned beers right in or at the end of the baby care aisle. While it might seem odd, the data shows a powerful link. This makes it convenient for tired parents to grab both items at once, boosting your sales.
Discovery: The "Breakfast Staples" Combo
What the data says:
{rolls/buns} → {whole milk}: When a customer buys rolls or buns, there's a 67% chance (2 out of 3 times) that they'll also pick up whole milk.
Lift > 2: This pairing happens twice as often as random chance would suggest, so it's a reliable pattern.
Support = 0.16: This is a fairly common shopping trip, found in 16% of all transactions.
Actionable Insight: This is a classic combination, likely for breakfast or sandwiches. To capitalize on this, you can:
Bundle Deal: Offer a small discount when someone buys both rolls/buns and whole milk together (e.g., "Breakfast Bundle: Save 50 cents!").
Cross-Promote: Place signs in the bakery section near the buns that say, "Don't forget the milk!" with a picture of your whole milk brand.
Strategic Placement: Ensure your fresh rolls and buns are on a clear path to the dairy aisle. If they are far apart, consider a small refrigerated display with milk near the bakery.
Your data has uncovered two very clear customer behaviors. By making small changes to your store's layout and promotions based on these insights, you can make shopping more convenient for your customers and increase the value of each sale.
Beyond the Basics
Add more transactions for richer patterns.
Try three-item rules with a more complete Apriori implementation.
Expose results in a dashboard with Qute or PrimeVue.
Scale with Kafka or Debezium if transactions are streaming in real time.
You’ve just built a data-mining detective in Java. From Quarkus setup to Apriori rules, you’ve uncovered how simple code can translate into powerful business insights.
The Quarky Cart is ready to sell more beer with its diapers. And you? You’re now a supermarket sleuth.