Stable Chores with Java: Building a Fair Assignment API Using the Gale-Shapley Algorithm
Build a REST API That Distributes Household Tasks Fairly
Every family, roommate group, or shared living situation faces the same weekly dilemma: Who does what chore? Assigning tasks sounds simple, until everyone shares their preferences.
Let’s say Alex likes doing laundry but hates doing dishes. Ben loves dishes but would rather avoid trash duty. Chloe doesn’t care much but prefers not to do the same thing two weeks in a row. If we just randomly assign chores, we’re likely to spark complaints.
Enter the Gale-Shapley algorithm, a well-known approach from computer science for solving "stable matching" problems. Originally designed to match medical students to hospitals, it turns out to be perfect for assigning chores fairly, ensuring that nobody has a strong reason to want someone else’s chore.
In this tutorial, you’ll build a Java REST API using Quarkus that accepts a list of preferences and returns a stable, conflict-free assignment of chores. The implementation will be beginner-friendly but grounded in solid principles of data modeling and algorithm design.
Project Setup with Quarkus
We'll start by generating a clean Quarkus project with the REST and JSON extensions required for building our API.
Make sure you have JDK 17+ and Maven installed. Then, in your terminal, run:
quarkus create app com.example:household-chore-assignment \
--extension='rest-jackson' \
--no-code
cd household-chore-assignment
This sets up the scaffolding. The rest-jackson
extension gives you everything needed to expose JSON REST endpoints using JAX-RS.
And make sure to check out my Github repository with the source code in case you don’t want to follow along. There’s also plenty more for you to explore.
Define the Input Data
The chore assignment logic needs to know two things:
What chores each family member prefers (ranked).
Which family member is most suitable for each chore (also ranked).
To represent this, create a new record class:
src/main/java/com/example/ChoreAssignmentRequest.java
package com.example;
import java.util.List;
import java.util.Map;
public record ChoreAssignmentRequest(
Map<String, List<String>> familyPreferences,
Map<String, List<String>> choreSuitability) {
}
This structure maps directly to a JSON object. Using Java records helps keep our data model clean and immutable.
Implement the Gale-Shapley Algorithm
Now for the core logic. We'll write a service class that processes the input data and applies the Gale-Shapley stable matching algorithm.
Create the following file:
src/main/java/com/example/ChoreAssignmentService.java
package com.example;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ChoreAssignmentService {
public Map<String, String> findStableAssignment(ChoreAssignmentRequest request) {
Map<String, List<String>> memberPrefs = new HashMap<>(request.familyPreferences());
Map<String, List<String>> chorePrefs = request.choreSuitability();
Map<String, String> assignments = new HashMap<>();
Queue<String> unassignedMembers = new LinkedList<>(memberPrefs.keySet());
Map<String, Map<String, Integer>> choreRankings = new HashMap<>();
chorePrefs.forEach((chore, prefs) -> {
Map<String, Integer> ranks = new HashMap<>();
for (int i = 0; i < prefs.size(); i++) {
ranks.put(prefs.get(i), i);
}
choreRankings.put(chore, ranks);
});
while (!unassignedMembers.isEmpty()) {
String currentMember = unassignedMembers.poll();
List<String> memberChorePrefs = memberPrefs.get(currentMember);
if (memberChorePrefs.isEmpty())
continue;
String preferredChore = memberChorePrefs.remove(0);
if (!assignments.containsKey(preferredChore)) {
assignments.put(preferredChore, currentMember);
} else {
String currentAssignee = assignments.get(preferredChore);
Map<String, Integer> currentChoreRanks = choreRankings.get(preferredChore);
if (currentChoreRanks.get(currentMember) < currentChoreRanks.get(currentAssignee)) {
assignments.put(preferredChore, currentMember);
unassignedMembers.add(currentAssignee);
} else {
unassignedMembers.add(currentMember);
}
}
}
Map<String, String> finalAssignments = new HashMap<>();
assignments.forEach((chore, member) -> finalAssignments.put(member, chore));
return finalAssignments;
}
}
This method walks through each unassigned family member, having them propose their top remaining chore. Chores “accept” or “reject” proposals based on how much they prefer the new proposer to their current match. It’s simple but powerful—and ensures stability.
Expose the API
Now let’s make the service available over HTTP. We'll create a POST endpoint that accepts a ChoreAssignmentRequest
and returns a JSON map of final assignments.
src/main/java/com/example/ChoreResource.java
package com.example;
import java.util.Map;
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;
import jakarta.ws.rs.core.Response;
@Path("/assign-chores")
public class ChoreResource {
@Inject
ChoreAssignmentService assignmentService;
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createChoreAssignment(ChoreAssignmentRequest request) {
Map<String, String> stableAssignment = assignmentService.findStableAssignment(request);
return Response.ok(stableAssignment).build();
}
}
This is a classic Quarkus REST endpoint. It delegates the real logic to your service and returns the result in a clean, JSON-friendly format.
Run and Test
Let’s try it out.
First, start the dev server:
./mvnw quarkus:dev
In a second terminal, send a test request with curl
:
curl -X POST -H "Content-Type: application/json" -d '{
"familyPreferences": {
"Alex": ["Laundry", "Dishes", "Trash"],
"Ben": ["Dishes", "Laundry", "Trash"],
"Chloe": ["Trash", "Laundry", "Dishes"]
},
"choreSuitability": {
"Dishes": ["Ben", "Alex", "Chloe"],
"Laundry": ["Alex", "Chloe", "Ben"],
"Trash": ["Chloe", "Alex", "Ben"]
}
}' http://localhost:8080/assign-chores
If successful, you’ll receive:
{
"Ben": "Dishes",
"Alex": "Laundry",
"Chloe": "Trash"
}
That’s a stable matching. Everyone got something that works based on their preferences and there’s no incentive for anyone to swap.
Wrapping Up
You’ve just built a REST API that applies a classic algorithm to a practical, everyday problem. The Gale-Shapley algorithm may have been invented for medical matching, but it works beautifully for chore wars too.
This is a great foundation for deeper exploration. You could extend it to:
Add historical data and track fairness over time
Support chore rotation
Build a web UI with Qute templates
Store inputs and results in a database using Panache
For more on Quarkus:
Happy chore-assigning!