Three small tools, three lessons
A vaccine-slot checker from 2021, a WebSocket chat on a free tier that no longer exists, and a Docker setup that streams five videos to YouTube at once. Small projects, each with one lesson that applies to big systems.
Not every project needs to be a platform. Some of the most useful lessons I’ve learned came from small tools built in an evening to solve one problem. Here are three from my GitHub, and the lesson each one taught me.
1. The vaccine-slot checker: ship the thin slice
cowin-vaccine-availability · Java 11, Spring Boot 2.5 · 2021
During India’s COVID-19 vaccination drive in 2021, booking a slot on the CoWIN portal was a race. Slots appeared and were gone within seconds, and there was no notification — you refreshed and hoped. The plan was simple: poll the public CoWIN API for my district and raise an alarm the moment a slot opened.
The version in the repository is deliberately the first slice of that plan: a Spring Boot app with one service that calls the public calendarByDistrict endpoint and returns the raw response through a REST endpoint.
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(cowinUri)
.queryParam("district_id", districtId)
.queryParam("date", date);
HttpEntity<Object> response = restTemplate.exchange(
builder.toUriString(), HttpMethod.GET, entity, Object.class);
It returns Object, with a // TODO: replace with object above it. That looks lazy, and in a production codebase it would be. But for a first slice it is the right call: before modelling a response, look at it — which fields are always present, how capacity is split by dose and age group, what an empty day looks like. Later, community Telegram bots did this at scale and better, and that was fine: the problem got solved.
The lesson: build the thinnest slice that touches the real system, look at what comes back, then design. On large projects this is the difference between a migration plan built on assumptions and one built on what the data actually looks like.
2. The WebSocket chat: anything free can disappear
spark-websocket-chat · Java, Spark Java, Jetty WebSockets
A group chat on the lightweight Spark Java framework, using Jetty’s WebSocket support underneath. It kept every connected session in memory and broadcast each message to all open sessions, with the list of active users attached:
webSocketSession.getAllActiveSession().stream().filter(Session::isOpen).forEach(s -> {
s.getRemote().sendString(new JSONObject()
.put("response", wrapMessageInArticle(senderName, message))
.put("activeUsers", webSocketSession.getAllActiveUsername())
.toString());
});
No database, no message log — privacy by design, because nothing was stored. Idle connections closed after 10 minutes (webSocketIdleTimeoutMillis(10 * 60 * 1000)), and the README notes it handled more than 500 concurrent users. I set up a pipeline to deploy it to Heroku’s free tier and put a live demo link in the README.
Then, on November 28, 2022, Heroku discontinued free dynos. The link in the README now leads nowhere.
The lesson: anything you don’t pay for — a free tier, a vendor’s goodwill, an unversioned internal API owned by another team — can change under you. Keep the deployment reproducible from the repository (here: build a fat JAR, run java -jar) so moving is a chore, not a rewrite. In system design terms: know which dependencies you control, and have an exit plan for the ones you don’t.
There’s a second, smaller lesson in that broadcast loop: it sends to every session one after another on the same thread. That’s fine for hundreds of users; at thousands, one slow client delays everyone. A per-session send queue is the fix, and it’s the same pattern you’ll find inside any serious messaging system.
3. The 5-stream broadcaster: do the arithmetic first
yt-multi-stream · Docker Compose, ffmpeg
The goal: stream five different local videos to five YouTube live streams at once from one machine. The setup is five identical containers, each running a small script that loops one video into ffmpeg and pushes it to YouTube’s RTMP ingest with its own stream key:
INPUT="/videos/${VIDEO_FILE}"
OUT="rtmp://a.rtmp.youtube.com/live2/${STREAM_KEY}"
VIDEO_BITRATE="${VIDEO_BITRATE:-13500k}"
PRESET="${PRESET:-ultrafast}"
Everything is configured through environment variables, so one script serves all five containers, and docker compose up -d starts the lot.
The interesting part is the arithmetic. The defaults target 1440p at 13.5 Mbps per stream. Five streams need 67.5 Mbps of sustained upload, before audio and overhead. Most home and many office connections can’t do that. ultrafast keeps CPU per stream low, so the first thing that breaks isn’t the machine — it’s the network, and YouTube reports it as buffering. The README’s advice is accordingly blunt: lower the bitrate, lower the resolution, or run fewer streams.
The lesson: before scaling out, multiply. Requests per second × payload size, consumers × partitions, streams × bitrate. A one-line calculation tells you which resource runs out first, and it’s often not the one you were planning to add more of.
What the three have in common
None of these projects is big. Each one met a real constraint — an API I didn’t control, a platform that changed its terms, a network with a ceiling — and each constraint is one that large systems meet too, just with more zeros. Small tools are a cheap place to learn expensive lessons.