← Back to portfolio

Case study · 2025

Extrabite

Surplus food and the people who need it are rarely separated by supply. They are separated by coordination - who has what, for how long, and who can collect it before it stops being food. Extrabite is my attempt to put that coordination in software.

My role
End to end — schema, API, frontend, deployment
Backend
Java 17, Spring Boot 3.5, PostgreSQL, Spring Security, Docker
Frontend
React, Tailwind CSS, Google Maps

The problem worth solving

A restaurant with thirty unsold meals at 10pm and a shelter two kilometres away is not a logistics problem in the usual sense. Nobody needs a fleet. What is missing is a shared, current picture: what exists right now, where, for how long, and whether someone has already claimed it.

Before software, that picture lives in WhatsApp groups and phone calls. It goes stale within minutes, and the cost of being wrong is not an inconvenience - it is food thrown away, or somebody travelling to collect something that is already gone.

So the interesting engineering here is not CRUD. It is keeping the listing honest and proving the handover actually happened. Most of what follows is about those two things.

How it fits together

A React front end on Vercel, a Spring Boot service in a Docker container, and PostgreSQL underneath. Every request crosses two authentication filters before it reaches a controller, and a set of scheduled jobs runs against the same database on its own clock.

Browser React + Tailwind API key filter JWT filter + blacklist Controllers REST Services rules + state PostgreSQL JPA / Hibernate every request crosses both Scheduled jobs donation expiry · token cleanup · every 10 min
Two filters guard every route; the schedulers reach the same tables without going through them.

Three decisions worth explaining

1. Two layers of authentication, not one

A request has to answer two different questions before it does anything. The first filter checks an EXTRABITE-API-KEY header: is this call coming from the platform at all? The second reads the bearer token: and who is making it?

These are genuinely separate concerns, and conflating them is a common mistake. The API key does not identify a person and grants no permissions - it only keeps the surface from being a completely open API that anyone can enumerate. Authorisation is entirely the JWT's job, carried in claims for id, name, email, role and whether the profile is active.

What it does not buy: an API key shipped to a browser is not a secret - anyone can read it out of the network tab. It raises the cost of casual scraping; it is not a security boundary, and I did not treat it as one. Every route that matters still requires a real token.

2. Stateless tokens that can still be revoked

JWTs are attractive because the server does not have to remember anything. That is also their problem: a token stays valid until it expires, so "log me out everywhere" is not something the standard gives you.

I chose to give up part of that statelessness. Logging out writes the token into a blacklist table, and the JWT filter checks that table before it trusts any token. A scheduled job clears out entries whose tokens have expired anyway, so the table tracks active sessions rather than growing forever.

The trade-off, stated plainly: this costs one database lookup on every authenticated request, and it means the service is no longer purely stateless. I took that deal because a logout that does not actually log you out is a security bug, not a performance feature - and at this scale one indexed lookup is cheap.

At a larger scale the same design still works, but the lookup belongs in Redis or a short-TTL in-memory cache rather than the primary database.

3. An OTP handover that proves the pickup happened

This is the part I would want to talk about in an interview. When a fulfiller accepts a request, the obvious design is to let them mark it complete afterwards. That design is wrong: it lets one side of a two-party event decide, on their own, that the event occurred. On a platform with ratings attached to completions, that is an invitation.

So the completion is split so that neither party can produce it alone:

  1. A requester posts what they need. The request is OPEN.
  2. Someone offers to fulfil it. The request becomes OFFERED.
  3. The requester accepts. The status moves to AWAITING_PICKUP and a six-digit pickup code is generated.
  4. The code is returned only to the requester, through an endpoint the fulfiller cannot call. The fulfiller never sees it in any response.
  5. At the handover the requester reads the code out. The fulfiller submits it. Only then does the request become COMPLETED.

The code is worthless without physical presence, which is exactly the property wanted: a completed request now means two people actually met. The same six digits also settle disputes, because a completion cannot exist without one.

Keeping the listing honest

A donation board that shows food which is no longer edible is worse than no board at all, because somebody travels for nothing. Freshness expires in two different ways here, so there are two jobs rather than one general-purpose one.

Both run every ten minutes and both can be switched off at runtime, which mattered during testing: seeding a realistic board is impossible if a scheduler keeps expiring your fixtures underneath you.

Browsing that board is built on JPA Specifications rather than a repository method per filter combination. Status, food name, location, free-or-paid and expiry are each a predicate, and the browse endpoint composes whichever ones the caller supplied. Five independent filters are thirty-one method names the other way; this way they are five predicates.

What I would build differently

This shipped, it was tested by 50-odd people during the pilot, and the coordination it replaced was manual. It also has decisions in it I can now argue against - which is the more useful half of any project.

What I took from it

The parts I am still pleased with are the ones where I asked what the software has to prove, not what it has to store. A pickup code is a schema detail until you notice it is the only evidence that two strangers met. An expiry job is a cron entry until you notice it is the reason someone does not waste a trip.

The parts I would redo are almost all the same mistake: something that was obviously fine at the size I was testing, and obviously not fine one order of magnitude later. Writing them down is cheaper than rediscovering them.

Alok Maurya · Software Engineer

← Back to portfolio