We often conflate "messy code" (which is linear debt) with "structural coupling" (which is exponential debt). I've been looking at the trajectory of projects that hit the "10k user wall," and the pattern is always the same: early velocity was high because they coupled everything, but now every schema change requires a maintenance window and every API tweak breaks the mobile client.
Here is the specific architectural debt that actually matters (and acts like compound interest), based on my scars from migrating legacy monoliths:
First, the Integer vs. UUID debate. I used to be in the "Integers are faster/smaller" camp. But if you've ever had to merge two databases from an acquisition, or shard a database where ID collisions are mathematically guaranteed, you know the pain. Migrating from Ints to UUIDs in a live system involves locking tables and rewriting foreign keys across the entire stack. It’s a nightmare. The storage cost of UUIDs is negligible compared to the cost of that migration. Just use UUIDs (specifically v7 for sorting) from day one.
Second, the database schema rigidity. The "Monolith First" advice often leads to a strictly normalized schema that requires an `ALTER TABLE` for every feature. Once the table hits a few million rows, those migrations start timing out or locking the DB. The pattern that seems to work best is what I call the "Mullet Schema": business-critical data (auth, billing) in strict columns, but everything else (user preferences, feature configurations) in a JSONB column. Postgres JSONB is performant enough now that it effectively kills the need for a separate Mongo instance for 90% of use cases. It lets you iterate on features without database migrations.
Third, the "Velocity Cross." Everyone knows a monolith is faster for the first 6 months. You don't have the "setup tax" of distributed tracing, eventual consistency, or separate deployments. But somewhere around month 12, or the 10k user mark, the lines cross. In a monolith, I've seen dev time shift to about 70% "fighting the architecture" (preventing side effects) vs 30% shipping features. Service boundaries—even just coarse-grained ones like separating Auth/Billing from Core App—preserve that feature velocity.
The trade-offs are real, though. Microservices (or even just "services") suck at the beginning. You spend your first month writing Docker compose files and fighting with inter-service communication instead of building the product. Debugging a request that hops through three services is objectively harder than debugging a function call. If your project never grows past 1,000 users, you absolutely wasted your time with services.
But I'm starting to think "Monolith First" is dangerous advice unless you explicitly plan to throw the code away. It optimizes for a timeframe (0-6 months) that isn't the long-term reality of a successful business.
I'm curious where others draw the line in 2024. Has the tooling (K8s, managed infra) lowered the microservice tax enough to start decoupled? Or is the pain of `ALTER TABLE` on a 10GB monolith actually manageable if you're not an idiot?
Also, am I the only one who finds UUIDs annoying to debug visually, despite them being architecturally superior?