# Final Review of My Full Build: Insights and Key Changes URL: https://madhudadi.in/blog/posts/full-build-review-lessons-learned-and-key-changes Published: 2026-06-19 Tags: Architecture, Production Read time: 12 min Difficulty: intermediate > 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.# 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 | Metric | Value | |--------|-------| | Build time (first commit to first post) | 6 months | | Lines of Python (backend) | ~15,000 | | Lines of TypeScript (frontend) | ~25,000 | | API routers | 27 | | Database models | 20 | | Docker containers | 4 | | Monthly cost (server + domains + APIs) | ~$18/month | | Monthly visitors (first 30 days) | ~2,000 | | Posts published | 17 (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 | Category | Monthly | Notes | |----------|---------|-------| | VPS (Hetzner CX22) | $12 | 2 vCPU, 4GB RAM, 80GB SSD | | Domain | ~$1 | Amortized annual cost | | OpenAI API | ~$3 | Embedding + LLM calls | | Razorpay fees | ~$1 | Per-transaction on premium subs | | Backblaze B2 | ~$0.50 | Database backups | | Email (Resend) | ~$0 | Free 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.*