AWS Builder Center
Scarcity Is an Allocation Problem: Building a Heatwave Water Dispatcher at Environmental Hacks

Scarcity Is an Allocation Problem: Building a Heatwave Water Dispatcher at Environmental Hacks

Heatwaves push tanker water below demand, and first-come or nearest-first dispatch keeps shorting the same wards. We built JalPriority AI at Environmental Hacks: an XGBoost shortage model, an OR-Tools optimizer and a deprivation-debt ledger that carries past neglect into today’s priority, with human approval and a DynamoDB backend on AWS.

Imagine a municipal dispatch room on the fourth day of a heatwave. There are ten wards asking for water, a handful of tankers in the depot, and enough water for maybe two-thirds of the demand. Someone has to decide which wards get served first, how many litres each one gets, and which tanker drives where.
In practice that decision is usually made by habit. Whoever called first, whoever complained loudest, or whoever is closest to the depot gets water. Each of those rules sounds reasonable on a single day. Run them for a week and a pattern appears: the same distant, quieter wards end up at the back of the queue again and again.
That pattern is what our team, Team Avengers, set out to fix for the Heat and Water track of Environmental Hacks, part of the Bharat Builds Tour powered by AWS Builder Center and WeMakeDevs. We kept coming back to one idea. Water scarcity is not only a supply problem. It is also an allocation and fairness problem.

The problem nobody's software answers

Heatwaves push water demand up at exactly the moment supply gets tight. In many Indian cities, tanker water fills that gap, and tanker dispatch is where the hard choices get made.
When we looked at the tools that already exist, we found plenty of them. There are tanker booking apps, GPS tracking, delivery scheduling, routing optimizers and monitoring dashboards. They record requests, sequence them and track trucks. None of them answers the actual rationing question: when total demand exceeds total supply, how do you split the water fairly and still produce a plan the fleet can physically execute?
The common fallback rules each fail in a specific way:
  • First-come, first-served favours early and loud requesters, not the wards with the greatest need.
  • Nearest-first minimises driving, so hard-to-reach wards keep waiting.
  • Generic routing optimizers minimise cost and distance, with no memory of who was underserved yesterday.
There is a second gap too. When a tanker breaks down or supply drops mid-shift, the plan is usually reworked by hand. That is slow, and it is exactly when mistakes happen.

What we built: decision support, not autopilot

JalPriority AI is a decision-support prototype for municipal water-dispatch officers. Given a crisis snapshot (demand per ward, available supply, the tanker fleet and the heat alert level), it predicts where shortages will be worst, produces a feasible tanker dispatch plan, explains every allocation, and waits for a human to approve it.
We deliberately kept the design narrow: one ML model and one decision engine. XGBoost estimates shortage. OR-Tools decides allocation. Everything else (fairness tracking, baselines, explanations, approvals) is plain, testable application logic around those two pieces.
The backend is Python end to end, built on FastAPI, so the model and the optimizer sit in the same process and the API contract is published as typed OpenAPI schemas. The code follows a strict three-tier layout: the dashboard only calls the API, the API only calls services, and only the repository layer talks to storage.
A plan is produced in this order:
  1. The dashboard sends a scenario id and the officer's fairness weights to the API.
  1. The prediction service returns expected shortfall per ward, with its top contributing features.
  1. The optimization service solves allocation and routing under hard constraints.
  1. The baseline service runs first-come-first-served and nearest-first on the same resources.
  1. The metrics service computes seven comparison metrics and a reason for every ward.
  1. The repository layer saves the plan; nothing is dispatched until an officer approves it.
  1. On approval, the deprivation-debt ledger updates for the next dispatch window.
The deprivation-debt ledger is the piece no off-the-shelf router has. DynamoDB is built, tested with Moto, and running live.
All data in the prototype is synthetic, generated with fixed random seeds so anyone can rerun the same scenario. We label it that way everywhere, including here.

Step one: predict need, not complaints

The first thing we wanted to replace was complaint volume as a proxy for need. So the model does not predict demand in the abstract. It predicts unmet litres per ward, defined as the larger of zero and (estimated need minus water received). Its inputs are heat severity, demand history, delivery history and population.
We chose XGBoost regression because it handles small tabular datasets well, trains in seconds, and exposes feature importances that we can show the dispatcher next to each prediction.
The more important decision was what to compare it against. We benchmarked the model against the simplest reasonable forecast: assume tomorrow's shortfall looks like the last observed one. On held-out synthetic data, the model's mean absolute error was 884 litres versus 1,162 litres for the last-observed baseline, roughly 24% lower.
That number shows the model learned the structure of our simulator better than a naive rule did. It does not tell us how the model would perform on a real city's delivery logs, which are messier, sparser and biased by who was able to report a shortage. We report it as a simulation result and nothing more.
We also decided early that even a perfect forecast would not solve the problem. Knowing that Ward 7 will be 3,000 litres short does not tell you whether to serve it before Ward 2, which was shorted twice last week. That is the optimizer's job.

Step two: turn fairness into a number the solver can see

The optimizer, built with Google OR-Tools, takes the predicted shortfalls and produces two things: how many litres each ward gets, and which tanker delivers them in what order.
Some rules are hard constraints that a plan can never break. Total allocation cannot exceed available water. No tanker can carry more than its capacity. Routes must be feasible. If any of these is violated, the plan is wrong, so our target for constraint violations is exactly zero.
Fairness lives in the objective. The optimizer minimises priority-weighted unmet litres plus a travel-cost term:
Plain text
minimize Σ p_i · u_i + δ · travel
where p_i = 1 + β · D_i + γ · V_i

Here u is a ward's unmet litres and V is a vulnerability indicator, used only where a defensible one exists. D is deprivation debt.
Deprivation debt is a running ledger. After every approved dispatch window, each ward's unmet litres are added to its debt:
Plain text
D_i(next) = D_i + u_i

A ward that was shorted yesterday therefore carries a higher priority weight today. The effect is that neglect compounds into priority instead of disappearing. A ward cannot be quietly pushed to the back of the queue twice in a row without the system noticing and correcting for it.
The weights β, γ and δ are not hidden in code. They are explicit, shown on screen, and adjustable by the officer. Whether debt should decay over time, or be capped, is a policy question for a municipality to decide, so we left it as a parameter rather than hard-coding our own answer.

Step three: make every trade-off visible

A fairness-aware optimizer is easy to claim and hard to trust. So every scenario runs through three strategies on identical resources: first-come-first-served, nearest-first, and JalPriority. All three are scored on the same seven metrics:
Metric
Better when
Priority-weighted unmet demand
Lower
Deprivation debt remaining
Lower
Total unmet litres
Lower
Minimum ward coverage (lowest delivered-to-need ratio)
Higher
Total travel distance (km)
Lower, within reason
Constraint violations
Must be 0
Replanning time (seconds)
Lower
This comparison matters more than any single number. A fairer plan may drive more kilometres than nearest-first, and that is a real cost. Putting both numbers side by side lets an administrator see the trade-off and decide whether it is worth it, instead of taking our word for it.
Three more pieces make the system usable in an accountable setting:
  • Explanations. Every ward gets a plain-language reason for its allocation, generated from structured outputs of the optimizer rather than from a second model. Unserved demand is always shown, never hidden.
  • Human approval and audit. A plan is a recommendation until an authenticated officer approves, edits or rejects it. Every decision is written to an audit log.
  • Disruption replanning. If a tanker breaks down, supply is cut or a heat alert is raised, the officer applies that one change and gets a revised plan with a diff showing exactly which wards moved. The revised plan goes back through the same approval step.

The AWS layer: test locally, then go live

JalPriority is only useful if it remembers. The deprivation-debt ledger, every generated plan, every approval and every disruption has to persist across dispatch windows, and an administrator has to be able to replay a past decision. That makes storage a core part of the design, not plumbing.
We chose Amazon DynamoDB as the persistent store for three reasons:
  • The access patterns are key-based. We read and write by scenario, by plan and by ward, and we append events to a history. We do not need complex joins, which is where a key-value and document store fits well.
  • No database server to run. DynamoDB is fully managed, which matters for a small municipal team that will not have a database administrator on call during a heatwave.
  • Bursty load. During a real crisis, several officers may replan at once and then go quiet for hours. On-demand capacity suits that shape better than provisioning for the peak.
The architectural choice that made this practical was the repository layer. Services never talk to a database directly. They call a repository interface, and configuration decides whether that interface is backed by SQLite or DynamoDB. Locally, SQLite gives us a zero-setup database that runs anywhere. The DynamoDB implementation sits behind the same interface, so switching is a configuration change rather than a rewrite.
To test the DynamoDB path without a live AWS account in the loop, we used Moto, an open-source Python library that mocks AWS APIs in-process. Moto is not an AWS service. It lets our tests create tables and run real boto3 calls against a fake, in-memory DynamoDB, so the integration is exercised on every test run without credentials, network access or cost. A separate table-creation script defines the tables so the setup is reproducible rather than click-ops in a console.
Across the backend, the test suite currently has 111 passing tests, covering constraints, the debt ledger, metrics and storage. We also added Docker deployment-readiness checks.

From mock to live

The DynamoDB integration was built and tested locally with Moto first, then deployed. The tables are live on Amazon DynamoDB, and the application runs on Amazon EC2.
Our original plan also listed Amazon S3, Amazon SageMaker AI, AWS Lambda and Amazon API Gateway. We deliberately deferred them: the PRD's rule was a working optimizer and an honest evaluation first, AWS services only where they integrate cleanly. Moving to them is future work, not something we have built.
Going live is where a mock stops helping. Moto will not catch IAM permission errors or real network latency, so the run that matters is the same end-to-end scenario against the live table.

What fought back

Building two halves at once. With two people and a hard deadline, we could not build sequentially, so we split the system along its natural seam. One half took the path from need to plan: synthetic data, the shortage model, the optimizer, and the snapshot and plan screens. The other half covered the rest of the system. We agreed on shared data contracts first, built against them in parallel, and merged everything in a dedicated integration step.
Fairness does not come with a unit. Litres of unmet demand, kilometres driven and accumulated neglect are not the same kind of quantity, yet the objective has to add them up. Every weighting is a value judgement. We did not try to find the "right" weights. We made them explicit, adjustable and visible on screen, and we let the baseline comparison show what each setting costs.
Synthetic data flatters models. A simulator has cleaner structure than the real world, so a model trained on it will look better than it would on real logs. Our defence was to always report against a naive baseline and to label every result as simulated.
Keeping local development fast while targeting the cloud. We wanted DynamoDB in the architecture without making every test depend on AWS credentials. The repository abstraction plus Moto solved that, at the cost of maintaining two storage implementations behind one interface.

Build day at DTU

Environmental Hacks ran online from October 8 to 11, 2026. In the middle of it, on Saturday, October 10, the Bharat Builds Tour brought builders together at the Dr. B.R. Ambedkar Auditorium at Delhi Technological University for an in-person build day organised with AWS and WeMakeDevs.
The day opened with remarks from Kunal Kushwaha and a welcome from Jessica Gilmore of AWS, followed by a panel with Kunal Kushwaha, Saiyam Pathak and Piyush Garg. Chanpreet Singh and Shweta Chopra then ran a hands-on Strands Agents workshop. The afternoon and evening were build time with mentors around, ending in a project showcase. It landed in the middle of our own online build, and it was a chance to work in the same room as other teams attacking environmental problems from very different angles.

Where this fits in the Sustainable Development Goals

We think about environmental sustainability here as a resource-allocation problem: a fixed amount of water, more need than supply, and a decision that will be made one way or another. The question is whether it is made transparently.
  • SDG 6, Clean Water and Sanitation. The deprivation-debt ledger and the minimum-coverage metric target the specific failure where scarce water reaches some communities repeatedly and others rarely.
  • SDG 11, Sustainable Cities and Communities. Instant replanning and audit trails are about municipal service delivery that holds up under stress, and that can be explained afterwards.
  • SDG 13, Climate Action. Heatwaves are a climate adaptation problem. The shortage model uses heat severity directly, so the plan responds to the heat alert rather than to yesterday's request queue.
The same anti-neglect mechanism also speaks to SDG 10 (Reduced Inequalities), since it is designed to stop the same communities from being skipped over and over.
Everything above describes potential impact. Our results come from synthetic scenarios. We have not improved anyone's water access yet, and we will not claim to until the system is evaluated on real data with a real control room.

What stands between this prototype and a real control room

JalPriority is a working prototype on one simulated service area of 6 to 10 wards. Several things would have to be true before it belonged in a municipal dispatch room:
  • Real data. Historical delivery and demand logs from a water utility, reviewed against India's Digital Personal Data Protection Act, 2023, and kept at ward level rather than household level.
  • Defensible vulnerability indicators. The vulnerability term is only as fair as its inputs, and choosing them is a policy decision that has to be made with the municipality.
  • Scale. We have not tested larger service areas. Bigger instances will need solver time limits and heuristic routing.
The pilot we would aim for after the hackathon is deliberately modest: one water-control room, historical data, read-only advisory mode. The system suggests, officers compare it with what they actually did, and nobody's water depends on it until the evidence says it should.

What other hackathon builders can take away

  1. Ship the baseline before the model. A naive "last observed value" forecast is quick to write and is what gave our MAE a meaning. Without it, 884 litres is just a number.
  1. If you claim fairness, put it in the objective and on the screen. A fairness claim that lives only in the pitch deck is unfalsifiable. Explicit weights plus a side-by-side comparison against simple rules make it something a judge, or a city, can check.
  1. Put a repository interface in front of every cloud service. A local fallback (SQLite for us) means the demo never depends on a deployment finishing, and swapping in the cloud backend is a configuration change.
  1. Mock the cloud in your tests. Moto let us exercise real boto3 DynamoDB calls on every test run with no credentials and no bill. Just remember that a mock will not catch IAM or latency problems; only a live run will.
  1. For a two-person team, agree on contracts before writing code. Shared data schemas let both halves be built in parallel and merged in one integration pass.

Closing thought

We started with a question about water and spent most of our time on a question about memory: how should a system remember who it let down last time? A forecast says where the shortage will be, and a solver says what the fleet can physically do. Neither is fair on its own. Fairness came from carrying yesterday's neglect into today's priority, and from letting a human see, question and override the result.
JalPriority is not finished, and no city runs on it. What it does show is a way to ration scarce water where every shortfall is on screen and every ward's history counts toward its next delivery.

Tech stack

Layer
What we used
Status
API
Python, FastAPI, typed OpenAPI schemas
Implemented
Shortage model
XGBoost regression, scikit-learn
Implemented, evaluated on synthetic data
Allocation and routing
Google OR-Tools
Implemented
Storage (local)
SQLite
Implemented
Storage (cloud)
Amazon DynamoDB via boto3, table-creation script
Deployed live, tested with Moto
Testing
111 backend tests, Moto for DynamoDB mocks
Implemented
Packaging
Docker, deployment-readiness checks
Deployed on Amazon EC2
Data
Seeded synthetic scenario generator
Implemented

Links and acknowledgements

Thanks to WeMakeDevs and AWS Builder Center for running Environmental Hacks and the Bharat Builds Tour, to Delhi Technological University for hosting the build day, and to the speakers, workshop leads and mentors who gave their Saturday to it.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article