DEV Community

shaojie gong
shaojie gong

Posted on

"I'll need a database to query this later" — the most expensive sentence in solo dev

I was one step from ripping out my key-value store and putting in a real database. The reason I gave myself sounded responsible: "I'll want to query subscription data later." I have zero users. There is no data to query. I almost spent a day building a warehouse for an empty box.

Here's the setup. My extension's paid tier is validated by a tiny Cloudflare Worker. It stores exactly one kind of thing: given an email, is this person subscribed? That's it. email -> { active, plan, periodEnd }. A lookup by key, returning one record. No joins, no reporting, no "show me everyone who churned last month." Key-value storage (Cloudflare KV) does this in one line and costs nothing.

Then the thought crept in: "But what if I want to see all my subscribers? Revenue by month? Who's about to expire? KV can't do those queries. I should use a real database now, while it's early, so I don't have to migrate later."

That sentence — "so I don't have to migrate later" — is how solo devs talk themselves into building things nobody asked for. I was about to trade a working, free, zero-maintenance store for a heavier one (schemas, SQL, per-row billing) to serve reports I will not read, about customers I do not have.

Two things stopped me.

First: the reports I was worried about already exist, and someone else maintains them. It's called the Stripe dashboard. Revenue by month, active subscriptions, who churned, who's due to renew, MRR — it's all right there, built by a team, for free, better than anything I'd hand-roll off my own database. The "I need to query my data" itch was real; I'd just forgotten payments run through Stripe, and Stripe is the query layer. 90% of what I'd ever want to know is a tab I already have open.

Second: the migration I was avoiding is cheaper than the migration I'd be creating. Switching KV to a database now, before the logic is even verified end-to-end, means changing storage AND payment logic at the same time — two moving parts, more places to break, right before launch. Whereas if I ever genuinely outgrow KV, I'll migrate then, when I know exactly what tables I need because I'll have real queries real users are asking for. "Migrate later with knowledge" beats "migrate now on a guess."

So I kept KV. The whole detour cost me twenty minutes of doubt instead of a day of rewrite, and the product ships on the simple thing that already works.

The pattern I keep catching myself in: dressing up "I want to build the fancy version" as "I'm being responsible about the future." The future-proofing instinct is real and sometimes right — but at zero users, almost every "I'll need this later" is a guess, and guesses are the most expensive code you can write, because you maintain them forever and they were never needed.

The test I now run on myself: is this solving a problem I have, or a problem I imagine a future version of me might have? If it's the second one, and the cost is real today, I don't build it. I write it down and move on.

What's a piece of infrastructure you added "for later" that later never came for?

— building NotebookBloom in public, #11

Top comments (5)

Collapse
 
publiflow profile image
PubliFlow

The transition from a key-value store to a relational database is almost always cheaper in the long run, even if the initial setup takes a few extra hours. I have seen so many solo devs spend weeks writing custom indexing and pagination logic in Redis just to realize a simple SQL query with a composite index would have taken five minutes. The real hidden cost is the cognitive load of maintaining complex data transformations in your application layer instead of letting the database engine handle it. Have you found a specific breaking point where you finally decided the migration was worth the downtime, or do you have a set of rules for when to pull the trigger?

Collapse
 
shaojie profile image
shaojie gong

no breaking point yet. my trigger isn't user count; it's the first real product query that needs relationships or a secondary index and can't be answered by Stripe. right now every app request is still email → subscription status, which KV is exactly built for. when that changes, i'll dual-write and backfill — but i want the real query to design the schema, not a guess.

Collapse
 
publiflow profile image
PubliFlow

Waiting for a concrete query to dictate the schema is a solid way to avoid premature optimization. Your plan to dual-write and backfill when that first complex relationship actually hits shows you are treating the migration as an engineering task rather than a dreaded chore. How are you planning to handle the transition period during that dual-write phase to ensure no subscription status updates slip through the cracks?

Thread Thread
 
shaojie profile image
shaojie gong

i'd keep Stripe as the source of truth, so dual-write wouldn't be the safety net. i'd populate both stores from the same Stripe lookup, backfill, and compare reads before cutting over. validation already re-checks Stripe, so a missed webhook can leave a cache stale, but it doesn't become the final answer.

Thread Thread
 
publiflow profile image
PubliFlow

Treating Stripe as the absolute source of truth and using read-comparison for cutover is a much safer migration strategy than relying on fragile dual-writes. It perfectly highlights how deferring the local database until you actually need it prevents you from building complex sync logic prematurely. Have you found that the read-comparison phase reveals any unexpected latency issues when querying both Stripe and the local cache simultaneously?