Ash

Software Engineer · Still Figuring Things Out

Electrical engineer turned software engineer. I like building and fixing things.

Projects
Tales From My Adventures

How I Fit 64 Million Places Into 25 Gigabytes

I'm building Traverra, a community-driven places API — think Google Places, but open and wiki-style: anyone can add spots, and the community keeps them accurate. The first consumer is the trip-sharing app I work on with friends. For Traverra to be useful from day one, it needed global coverage, and that meant loading 64 million places into the database. That's where the trouble started.

Traverra's schema doesn't just store coordinates. Every spot tracks revision history, trust levels, moderation flags, hours, photos — the machinery that makes community data trustworthy. I ran the numbers on loading all 64 million places with that full schema: 300–500 GB in Postgres, north of $100 a month on a managed database. For a project with exactly zero users. I looked at cheaper self-hosted options, but that meant owning backups, security, and PostGIS configuration — all time I'd rather spend building the product. I was stuck: full coverage was too expensive, partial coverage made the product useless, and self-hosting traded a money problem for a time problem.

The fix was embarrassingly simple once I saw it: stop treating all 64 million places the same. I split the data into two layers in the same database. A lightweight, read-only reference table holds the raw Overture Maps dataset with a spatial index — global coverage in about 15–25 GB, because it stores little more than names, categories, and coordinates. A separate community table uses the full schema and starts completely empty. A spot only gets written there when a user actually interacts with it — edits, flags, verifies, adds hours or photos. The API checks the community table first and falls back to the reference layer when nothing's there.

Client App "spots near me?" Traverra API merge · dedupe · respond 1 · checked first 2 · fallback Community Table full schema: history · trust · hours starts empty — grows with use Overture Reference Table 64M places · read-only spatial index · 15–25 GB a user edit promotes the spot Full schema: 300–500 GB → split: ~25 GB

There's a quiet bonus in this design beyond the bill. Because every spot in the community table got there through a real interaction, the expensive layer fills up weighted toward the places people actually care about — I never had to decide which cities or regions to import upfront, because usage decides. And since each record tracks where its data came from, a spot's fields can graduate from "imported baseline" to "community-verified" as people confirm them.

The storage footprint dropped by more than 90%, the bill came down with it, and the architecture hasn't needed rethinking since.

PostgreSQL · PostGIS · Overture Maps · Node.js · API Design

The 60-Second Response That Should Have Taken Milliseconds

A customer reported that a web service they'd generated with our product was taking 60 seconds per request. Some context: our product turns legacy COBOL applications into web services. A request comes in as JSON or SOAP, a runtime engine translates it into something the COBOL application understands, and the answer gets translated back on the way out. These services respond in milliseconds even in the worst case — so 60 seconds wasn't "slow," it was broken. My job was to figure out why.

First step: reproduce it. I recreated the customer's setup locally with their programs and generated timing data to confirm the problem was real, not environmental. Then I wanted a baseline, so I built the most trivial control service I could — one that just adds two numbers — and timed it. A fraction of a millisecond. That one experiment ruled out the entire platform as the cause: whatever was wrong was specific to this service and its data.

Next I captured the HTTP traffic with Wireshark to see what was actually crossing the wire, and there it was: the request body had an array initialized with 100 null elements, and by the response it had ballooned to 1,000. All that memory allocated, serialized, and shipped to say exactly nothing — that stood out immediately.

It also pointed at a suspect. COBOL doesn't have dynamic arrays the way modern languages do — its tables are fixed-size, with a maximum declared at compile time. All those nulls made me suspect the translation layer was allocating and serializing the full maximum table size and padding the unused slots, instead of sizing to the actual data.

Client Runtime Engine JSON → COBOL Memory Pre-Allocation Tables sized to declared max COBOL App Processes data Response Serialization 1,000 null elements serialized The bottleneck Runtime Engine COBOL → JSON Client 60s response Fix: size tables to actual data → sub-millisecond

The runtime engine belonged to another team, so I partnered with them to trace the request through their layer — and the hypothesis held. The table allocation was being calculated from the declared maximum, and we changed it to size from the number of elements actually populated. Latency dropped from 60 seconds back into the sub-millisecond range, the wasted memory disappeared, and the fix shipped to every customer on the product.

Two things I'd do differently. I'd loop in the runtime team earlier — I spent a fair amount of time investigating solo, and since the bug lived in their layer, their context would have gotten us to root cause faster. And because nothing about the bug was specific to this customer's data, I'd add a regression test that checks serialized payload size against the actual element count, so this whole class of over-allocation bug gets caught before it ever reaches a customer.

COBOL · Web Services · Wireshark · Performance Analysis · Memory Optimization

The Trip-Sharing App Nobody Asked For (But We Needed)

Right after grad school, some friends and I kept running into the same annoying problem. We all loved traveling, and we'd constantly ask each other things like "where did you stay in Lisbon?" or "send me that list of places from your Tokyo trip." The answers always came back as scattered texts or a Google Doc that someone forgot to share. So we built an app.

The idea was simple: one place to save the spots you've been, organize them into collections, add friends, and share trips with them. No more digging through group chats. We went serverless on AWS — DynamoDB for storage, Cognito for auth, the whole stack deployed automatically.

I ended up owning most of the backend plumbing, and the part I'm proudest of isn't any single feature — it's everything around the code. I wrote unit tests with Jest against mocked DynamoDB calls. I wired up git hooks so the tests had to pass before a commit or push would go through. Then I built a full CI pipeline in Azure Pipelines that spun up a VM, ran the suite, and published coverage reports. None of this was required — we were building the app for fun — but I wanted to know what a real development workflow felt like, end to end.

That project taught me more about backend architecture, CI/CD, and working on a team than anything I did in school. It's also the reason I'm a software engineer at all — I came out of grad school with an electrical engineering degree, and building this app was what convinced me that shipping software was the thing I actually wanted to do. Turns out building something just because you and your friends want it to exist is the most fun way to learn.

AWS (DynamoDB, Cognito) · Serverless · Jest · Azure Pipelines · Node.js

From Signals to Software: What Adaptive Filtering Taught Me About Noisy Data

In grad school, I worked on multipath mitigation — what happens when radio signals bounce off buildings and terrain on their way to a receiver and arrive as a garbled mess of overlapping waveforms. GPS, passive radar, telecom — they all fight this problem. I simulated it in MATLAB and used adaptive filters (LMS and NLMS) to pull the original clean signal back out of the noise.

My first attempt — a basic two-tap LMS filter — flat-out didn't work. The weights oscillated and diverged instead of settling, and the error signal never died down. I spent a while tuning parameters, convinced I'd made a mistake somewhere, before accepting that the filter simply didn't have enough degrees of freedom to model the interference.

Bumping it up to eight taps helped a lot: the weights converged around 300 iterations and the error dropped to zero. But the real breakthrough was switching to NLMS, which normalizes each weight update by the energy of the input signal. With just two taps — the same two taps that had failed before — NLMS drove the error to near zero. And when I pushed the sampling rate higher, LMS fell apart all over again, weights oscillating with no sign of convergence, while NLMS stayed rock steady.

The lesson stuck with me: throwing more complexity at a problem — more taps, more iterations — doesn't always fix it. A fundamentally better algorithm can do more with less. That pattern shows up constantly in software too. I've caught myself brute-forcing web app problems with more code and more state when the real fix was rethinking the approach. Messy API responses, race conditions, state that won't settle — none of them look like radio signals, but the job is the same: pulling something clean out of something noisy.

Read the full paper (PDF) →
Adaptive Filtering · LMS/NLMS · MATLAB · Signal Processing