Full Build Review: Lessons Learned and Key Changes

Jun 19, 2026
12 min read

AI Insights

Powered by GPT-4o-mini

Verified Context: full-build-review-lessons-learned-and-key-changes
Quick Answer

Six months of building, 15 posts of writing. The numbers (build time, cost, traffic), what broke in production, what I'd do the same, and what I'd change for the next project.

Quick Summary

Explore the final insights from my full build project, including lessons learned, key metrics, and essential changes for future development.

The Full Build In Review: Lessons, Numbers, What I'd Change

This is the 15th and final post in the series. If you've read all the way through, you've seen the full architecture — from the monorepo structure to the RAG pipeline to the Docker stack on a $12 VPS.

This post is the honest retrospective: what worked, what didn't, and what I'd do differently.


The Numbers

MetricValue
Build time (first commit to first post)6 months
Lines of Python (backend)~15,000
Lines of TypeScript (frontend)~25,000
API routers27
Database models20
Docker containers4
Monthly cost (server + domains + APIs)~$18/month
Monthly visitors (first 30 days)~2,000
Posts published17 (and counting)

What Broke in Production

1. The Embedding Pipeline Ran Out of Memory

The first version of the embedding pipeline loaded all post content into memory, chunked it, and sent it to the embedding API in parallel. With 10 posts, this worked fine. With 50 posts, memory usage hit 1.2GB and the VPS (2GB RAM) started swapping.

Fix: Stream chunks one post at a time, with a concurrency limit of 3 concurrent embedding requests. Memory usage now stays under 300MB.

2. Redis Pub/Sub Lost Messages

The XP event system used Redis pub/sub without persistence. When the backend restarted during deployment, any XP events in the queue were lost — users who completed a post during the deploy window lost their XP.

Fix: Added a database-backed event queue alongside Redis pub/sub. Events are written to a pending_events table first, then processed asynchronously. The Redis channel is used for real-time notifications only.

3. The Sitemap Cached Stale URLs

Next.js ISR cached the sitemap for 24 hours by default. When a new post was published, it wouldn't appear in the sitemap until the next day.

Fix: Added a revalidation endpoint that purges the sitemap cache on publish:

typescript
// Called from backend after post publish
await fetch(`${SITE_URL}/api/revalidate?secret=${REVALIDATION_SECRET}&path=/sitemap.xml`);

4. Google OAuth Redirect URI Mismatch

The OAuth callback URL included a trailing slash in development but not in production. Google's OAuth validation is strict about exact URI matching.

Fix: Normalized the redirect URI configuration to always strip trailing slashes, and added a test case that validates the exact URI against Google's console configuration.

5. Premium Wall Showed Content Briefly

The premium wall is rendered client-side. For ~500ms between the page loading and the auth state resolving, the post content was visible to non-premium users.

Fix: Moved the premium check to the server component. The 403 response is returned before any HTML is sent. The premium wall is now rendered server-side, never client-side.


What I'd Do the Same

Monorepo over Microservices

For a single-developer project, a monorepo is the right choice. Shared types, single deploy, no cross-service versioning headaches. The backend and frontend communicate through a well-defined API contract, but they live in the same repository with the same CI pipeline.

PostgreSQL over Document DB

The relational nature of the data (users → progress → posts → series → badges) made PostgreSQL the clear winner. JSONB columns handle the semi-structured data. Joins handle the relationships. pgvector handles the embeddings. One database for everything.

FastAPI over Django

FastAPI's async-native design paid off in every endpoint that makes external API calls (embedding, LLM, Razorpay). The Pydantic schema integration meant request/response validation was automatic. The only thing I missed from Django was the admin interface — but building a custom admin in the frontend was straightforward.

No Third-Party CMS

Building the CMS from scratch was the right call. Every feature I wanted required deep integration at the database level. A headless CMS would have added API overhead and rate limits. A traditional CMS would have constrained the data model. Custom code was more work upfront but zero friction afterward.


What I'd Change

Add Tests Earlier

The backend has ~200 tests. The frontend has ~30 component tests and 10 E2E tests. Most of these were written after the features were built. Writing tests alongside the features would have caught several regressions earlier, especially around the authentication and payment flows.

Use a Proper Task Queue

Redis pub/sub for background tasks works but isn't durable. If the app crashes mid-job, the task is lost. ARQ or Celery with Redis broker would give retries, dead-letter queues, and persistent task storage. This is the change I'd make first if I were rebuilding.

Separate the Admin Router

The admin.py router handles post CRUD, user management, analytics, and maintenance tasks — all in 500+ lines. It should be 4-5 separate routers. The router grew organically and never got refactored.

Add OpenAPI Type Generation

The frontend API client is manually typed. When the backend adds a field, the frontend types need a manual update. Using openapi-typescript to generate types from the FastAPI OpenAPI schema would eliminate this class of bugs entirely.


What It Cost

CategoryMonthlyNotes
VPS (Hetzner CX22)$122 vCPU, 4GB RAM, 80GB SSD
Domain~$1Amortized annual cost
OpenAI API~$3Embedding + LLM calls
Razorpay fees~$1Per-transaction on premium subs
Backblaze B2~$0.50Database backups
Email (Resend)~$0Free tier
Total~$17.50

The server handles ~200 requests/minute during peak hours with 30% CPU utilization and ~1.5GB RAM usage.


What I'd Tell Someone Building Their Own

Start smaller. The first version didn't need RAG, gamification, or a knowledge graph. It needed: write a post, read a post, format it well. Everything else could have waited.

Ship faster. Six months of building before the first post was too long. I should have shipped the MVP in 6 weeks and iterated based on real reader feedback. The gamification system was built twice — the first version was wrong because I hadn't watched real users interact with the content.

Write the series posts as you build. Documenting the RAG system six months after building it meant reconstructing decisions from git history and memory. Writing each post during or immediately after implementation would have produced better documentation and caught design issues earlier.


What's Next for This Blog

The series is complete, but the blog continues. Planned improvements:

  • AI-generated challenges — daily coding challenges generated from recent posts
  • Community features — comments, discussion threads, user-contributed code examples
  • Mobile app — React Native wrapper for the PWA
  • More series — FastAPI deep-dive, PostgreSQL performance, Docker production patterns

Thank You

If you've read all 15 posts, you've absorbed the equivalent of a small book about building production web applications. I hope the series was useful — and if you're building something similar, I'd love to hear about it.


Built with FastAPI, Next.js 16, PostgreSQL, Redis, and — most importantly — the willingness to build it from scratch.

Frequently Asked Questions

What was the main issue with the embedding pipeline in production?
The embedding pipeline ran out of memory when processing 50 posts, causing the VPS with 2GB RAM to start swapping.
How was the Redis Pub/Sub message loss issue resolved?
A database-backed event queue was added alongside Redis pub/sub to ensure events are written to a pendingevents table first, preventing message loss during backend restarts.
What change was made to address the Google OAuth redirect URI mismatch?
The redirect URI configuration was normalized to always strip trailing slashes, and a test case was added to validate the exact URI against Google's console configuration.
How was the issue of the premium wall showing content briefly resolved?
The premium check was moved to the server component, ensuring a 403 response is returned before any HTML is sent, rendering the premium wall server-side.
Why was a monorepo chosen over microservices for this project?
For a single-developer project, a monorepo is preferred due to shared types, single deploy, and no cross-service versioning headaches.

Related Work

See how this thinking shows up in shipped systems.