<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Francisco Luna: Building from the Abyss]]></title><description><![CDATA[Principles of software architecture, leadership, and building high-performance systems for startups. ]]></description><link>https://blog.itsfranciscoluna.com</link><generator>RSS for Node</generator><lastBuildDate>Mon, 14 Sep 2026 23:25:04 GMT</lastBuildDate><atom:link href="https://blog.itsfranciscoluna.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[5 Lessons From a Failed Startup: How I Evolved from Developer to Systems Engineer]]></title><description><![CDATA[Context
I was a founding engineer for a startup, building B2C apps. We sunsetted the projects we had because they were not profitable, and the runway was over.
During my time in the startup, I led a s]]></description><link>https://blog.itsfranciscoluna.com/5-lessons-from-a-failed-startup-how-i-evolved-from-developer-to-systems-engineer</link><guid isPermaLink="true">https://blog.itsfranciscoluna.com/5-lessons-from-a-failed-startup-how-i-evolved-from-developer-to-systems-engineer</guid><category><![CDATA[AI]]></category><category><![CDATA[startup]]></category><dc:creator><![CDATA[Francisco Luna]]></dc:creator><pubDate>Mon, 29 Jun 2026 19:10:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/689f4f0d610919fe10ba3c12/0a8ff27b-4d7a-4739-a036-02834e01548d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Context</h3>
<p>I was a founding engineer for a startup, building B2C apps. We sunsetted the projects we had because they were not profitable, and the runway was over.</p>
<p>During my time in the startup, I led a small team and translated the business vision into actual products.</p>
<p>Let me share the main mistakes we made while trying to find product-market fit (PMF) in our products. This will be useful for solo founders and other CEOs/CTOs reading this blog. I hope you don’t make these mistakes:</p>
<hr />
<h3>1. Jack of all trades, master of none</h3>
<p>Imagine you’re building an application that combines Twitter, and Habitica. Let’s call it “Habits”. You have social feeds, user profiles, followers, privacy settings, and users can track habits, send messages, and share photos. The first thing every single user is going to ask is: “What is this application for?” Each feature could’ve been a different app…</p>
<p>My advice is to stick to a single idea and solve an existing problem, even if it’s niche. Founders create and make up problems; most of the time, they’re only anxious.</p>
<p>Great founders should focus on solving existing problems, not creating new ones.</p>
<hr />
<h3>2. Wrong pivots</h3>
<p>Remember the Habits app from the first point? Well, it was a public application using the B2C business model. The idea failed because there was no profit, even if users were still sharing posts and interacting in the app.</p>
<p>We pivoted, selling the same Habits app to organizations, allowing users to register with QR codes this time to increase retention rates. We put it in front of 10k users, and we only got 30 sign-ups in 2 weeks...</p>
<p>I learned the most valuable lesson with pivots: you cannot use the same method and expect different results.</p>
<hr />
<h3>3. It's just a small feature</h3>
<p>I remember the first time I had to build chat rooms with the Habits product. It went okay, and it was a great learning experience. Later, I was asked to allow users to share habits with others via DM.</p>
<p>Suddenly, we were building 40 React components, fighting with responsive design, extending the core API, and creating multiple user stories in Excalidraw with the team, only to share a list...</p>
<p>Days later, we found out no one was using this feature, wasting 17 days of engineering and months of planning.</p>
<p>My suggestion is the following: add the features the user requests and what the business needs; don’t build without studying your users and metrics.</p>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/1q5irgw3exh428261dvi.png" alt="Chat feature example" style="display:block;margin:0 auto" />

<hr />
<h3>4. Marketing before product-market fit</h3>
<p>Don’t hire a marketing team until you have achieved at least 1 paying customer.</p>
<p>Marketing amplifies your value. If your value is 1, you’ll still get 1; if your value is 3, you’ll have 9.</p>
<hr />
<h3>5. Notes for developers</h3>
<ul>
<li><p>Use Terraform or Pulumi to define your infrastructure; clients need the architecture to be replicable.</p>
</li>
<li><p>Create runbooks. Document how database migrations run, how to set up the development environment, and how common product operations run.</p>
</li>
<li><p>Don't separate frontend from backend in an MVP. Don't do microservices.</p>
</li>
<li><p>Write tests and keep the product documented. This way, you can make changes to the code without worrying about it breaking.</p>
</li>
<li><p>Don’t fly blind in production. You need analytics, metrics, logging, and monitoring even for a single VPS.</p>
</li>
<li><p>Learn outside of work. Learn what you’re passionate about, contribute to open source, or build projects outside work.</p>
</li>
<li><p>Embrace AI and agentic workflows. You’ll still have to review the AI’s code and ensure you’re shipping the right thing to production.</p>
</li>
</ul>
<img src="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ikg8tpxypqptmf2bm1jh.png" alt="IaC illustration" style="display:block;margin:0 auto" />

<hr />
<h3>Conclusion</h3>
<p>Startups are hard, and most of the time, as an engineer, you cannot determine whether the product will be successful or if it's another lesson to learn from.</p>
<p>That's it. Those are the mistakes. Thanks for reading!</p>
<p>I'm building again. Available for Open Source collaborations, building Saas Products, and helping companies scale their products: <a href="https://www.itsfranciscoluna.com/">https://www.itsfranciscoluna.com/</a></p>
]]></content:encoded></item><item><title><![CDATA[Building Fabric: How I Automated Stakeholder Reports from GitHub]]></title><description><![CDATA[I’ve always hated weekly meetings with non-technical stakeholders.
Explaining technical work is hard. If you’re experienced, you know the drill: don’t mention webhooks or backend code. You say, "We ad]]></description><link>https://blog.itsfranciscoluna.com/building-fabric-how-i-automated-stakeholder-reports-from-github</link><guid isPermaLink="true">https://blog.itsfranciscoluna.com/building-fabric-how-i-automated-stakeholder-reports-from-github</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Francisco Luna]]></dc:creator><pubDate>Mon, 22 Jun 2026 02:26:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/689f4f0d610919fe10ba3c12/fac5fa7e-9790-4969-bc22-9d6362a97b61.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve always hated weekly meetings with non-technical stakeholders.</p>
<p>Explaining technical work is hard. If you’re experienced, you know the drill: don’t mention webhooks or backend code. You say, <em>"We added payments so customers can buy items,"</em> and keep it moving.</p>
<p>But then comes the request nobody talks about: <strong>the written report.</strong></p>
<p>In most small startups, there is no product manager. You have to write everything down for compliance or tracking. I found myself wasting hours re-reading my own code, checking the backlog, and filling out Notion pages to remember what I built.</p>
<p>It was a waste of time because everything I built was already logged in my Git history.</p>
<p>I wondered: <em>If my work is already tracked in my commits and pull requests, why am I writing these reports manually?</em></p>
<p>So, I built a tool to do it for me.</p>
<p>That is how <strong>Fabric</strong> started. An AI engine that turns your GitHub activity into finished reports for your stakeholders.</p>
<h2>Core Features</h2>
<p>I focused on building a tool that handles the heavy lifting of reporting so you don't have to:</p>
<ul>
<li><p><strong>Deep Repository Sync:</strong> Goes beyond simple line counts. It extracts real-time commit metadata to capture the actual intent of your work.</p>
</li>
<li><p><strong>Guided Onboarding:</strong> A clean, multi-step interface designed to get you set up quickly and reduce cognitive load.</p>
</li>
<li><p><strong>The Transparency Engine:</strong> Generates high-fidelity Markdown reports focused on business outcomes.</p>
</li>
<li><p><strong>Report Persistence:</strong> A centralized dashboard that lets you visualize and manage your historical reporting data.</p>
</li>
<li><p><strong>Persona-Driven Synthesis:</strong> Features custom tone mapping, allowing you to tailor reports specifically for CTOs, Founders, or Board Members.</p>
</li>
</ul>
<h2>The Tech Stack</h2>
<p>I kept the architecture simple to focus on the AI logic rather than managing complex infrastructure:</p>
<ul>
<li><p><strong>Next.js 16:</strong> The backbone of the application. Its speed and routing make it the perfect choice for a developer-focused tool.</p>
</li>
<li><p><strong>Groq API:</strong> This is where the magic happens. I chose Groq for its incredibly fast inference speeds, which are essential when feeding it batches of commit logs for summarization.</p>
</li>
<li><p><strong>Octokit:</strong> The official GitHub SDK. It handles the heavy lifting of interacting with the GitHub API.</p>
</li>
<li><p><strong>Cloudflare R2:</strong> Fabric extracts screenshots from PRs and re-hosts them on R2 (S3-compatible) to keep your internal tooling private without relying on GitHub authentication.</p>
</li>
</ul>
<h2>What’s Next</h2>
<p>Building Fabric has been about staying focused. To allow users to use Fabric faster, I’ve had to make some hard choices about what to ship later:</p>
<ul>
<li><p><strong>Platform Focus:</strong> Fabric is GitHub-only for now to prioritize the most common use cases. Support for GitLab and BitBucket is on the roadmap.</p>
</li>
<li><p><strong>Security &amp; Scaling:</strong> Currently, Fabric uses a simple, single-session experience without authentication. This lets individual developers and freelancers get immediate value without the friction of complex auth strategies.</p>
</li>
</ul>
<h3>Conclusion</h3>
<p>I'm already working on part two of this series, where I’ll dive into the technical architecture, prompt engineering and the core features.</p>
<p>Fabric is available at: <a href="https://github.com/franciscoluna-28/fabric-ai">https://github.com/franciscoluna-28/fabric-ai</a></p>
<p>Follow my journey and see my other work at <a href="http://itsfranciscoluna.com">itsfranciscoluna.com</a>.</p>
]]></content:encoded></item><item><title><![CDATA[I Built a Ferrari for a Skateboard: 5 Engineering Mistakes That Cost Me Months]]></title><description><![CDATA[We’ve all been there. You have a great idea, and you want to build something serious.
You start drawing up complex diagrams and setting up complicated systems. Before you know it, you’ve built a compl]]></description><link>https://blog.itsfranciscoluna.com/i-built-a-ferrari-for-a-skateboard-5-engineering-mistakes-that-cost-me-months</link><guid isPermaLink="true">https://blog.itsfranciscoluna.com/i-built-a-ferrari-for-a-skateboard-5-engineering-mistakes-that-cost-me-months</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[General Advice]]></category><category><![CDATA[Career]]></category><dc:creator><![CDATA[Francisco Luna]]></dc:creator><pubDate>Sun, 14 Jun 2026 23:28:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/689f4f0d610919fe10ba3c12/a9499ffc-676d-4a55-add6-5f93bb8005b6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We’ve all been there. You have a great idea, and you want to build something serious.</p>
<p>You start drawing up complex diagrams and setting up complicated systems. Before you know it, you’ve built a complex laboratory instead of an actual product.</p>
<p>I’ve been a founding engineer and a CTO, and I learned these lessons because I failed. I built things that didn't work, deleted databases I shouldn't have, and spent weeks fighting infrastructure that didn't need to exist.</p>
<p>That's why I’m sharing the mistakes that turned my projects into scalable pieces. Here is what I learned about building for reality, not for the resume.</p>
<h2>1. Be Careful with Distributed Systems</h2>
<p>When I started my projects, I thought a modern stack meant React, a separate backend, and a network of microservices. I treated my MVP as a critical system with millions of concurrent users.</p>
<p>The trust was simple. It was an idea that needed to be validated. When I created a distributed system for an MVP, I spent 80% of my time fighting network latency and deployment configurations, and 20% of my time actually building features. Here's when I learned the following:</p>
<p><strong>A monolith is the fastest path to market.</strong></p>
<p>If you aren't at the scale where you need to separate your services, don't use them. Don't pay the distributed system tax until you actually have the traffic to justify it.</p>
<h2>2. Manual Labor (Clicking in the Terminal)</h2>
<p>I’m sorry to say it, but I spent too much time clicking around the AWS/DigitalOcean dashboards like a trainee in existing projects. I built professional DevOps pipelines for projects with fewer than 100 users.</p>
<p>There I was, documenting the deployment setup using Notion and copying and pasting the same Linux commands...</p>
<p>If it’s not in code, it doesn’t exist. I learned the hard way that Infrastructure as Code (IaC) isn't just for major companies. Tools like Pulumi or Terraform should be your first commits. For any serious project, you should automate your infrastructure from day one, or accept that you’re going to be your own worst bottleneck.</p>
<h2>3. Building a Ferrari for a Skateboard Request</h2>
<p>The most important lesson? <strong>Know your scope.</strong></p>
<p>I built a Ferrari when the user and the market only needed a skateboard. I obsessed over the engine specs, the aerodynamics, and the fuel efficiency, but I forgot to check if the skateboard even rolled.</p>
<p>I created OpenAPI specifications and enterprise DevOps deployments for an MVP... I'm not proud of that.</p>
<p>If your goal is to validate an idea, build a skateboard. If the skateboard takes off, then you can build the Ferrari.</p>
<p>Don’t fall in love with the engineering at the expense of the product.</p>
<h2>4. Keep your Communication Simple</h2>
<p>Stop using technical jargon to justify business decisions. It doesn't make you look smart; it makes the project look complicated and risky.</p>
<ul>
<li><p><strong>Use Visuals:</strong> I started using a digital drawing tablet. It lets me sketch high-quality diagrams live in meetings, making complex ideas clear to everyone instantly.</p>
</li>
<li><p><strong>Focus on Results:</strong> Don't sell the tech stack. Sell the result. Instead of saying, "We're using X because of our microservice architecture," you can say, "We're using X to ship features twice as fast."</p>
</li>
</ul>
<h2>5. Master the Tools you Have</h2>
<p>Adding more tools won't fix a lack of mastery. Success comes from doing more with less.</p>
<p>Think like a musician: Michael Jackson didn't need 100 tracks to write a hit; he mastered simple, fundamental chords and arrangements.</p>
<p>If you can’t build a great product with a simple setup, adding more complexity won't save you; it will just hide the problem. Master your tools, stay within your constraints, and focus on providing value to your users.</p>
<h2>TL;DR</h2>
<ul>
<li><p>Always start with a monolith.</p>
</li>
<li><p>Use Terraform and set up your project properly. The foundation is everything.</p>
</li>
<li><p>Ask "does this need to exist?" before asking "can I build it?"</p>
</li>
<li><p>Communicate with diagrams and simple terms, not jargon.</p>
</li>
<li><p>Master fewer tools. Use them better.</p>
</li>
</ul>
<h2>Conclusion</h2>
<p>Thanks for reading. I’m currently pivoting my workflow to prioritize speed and simplicity in my next projects. If you’ve learned the hard way that less is more, I’d love to hear your war stories in the comments.</p>
<p>I'm also open to product engineering roles at <a href="https://www.itsfranciscoluna.com/">https://www.itsfranciscoluna.com/</a>, where I worked with brands like Edge Fitness on products I contributed to, maintaining 99.9916% uptime in the systems I built solo.</p>
]]></content:encoded></item><item><title><![CDATA[Startups, Pivots, and the Strangler Fig Pattern - June 2026]]></title><description><![CDATA[Recently at work, we've had to deal with a business pivot, yet, again. I wasn't happy to hear this after carefully planning the first version of the app, but my intuition told me it was going to happe]]></description><link>https://blog.itsfranciscoluna.com/startups-pivots-and-the-strangler-fig-pattern-june-2026</link><guid isPermaLink="true">https://blog.itsfranciscoluna.com/startups-pivots-and-the-strangler-fig-pattern-june-2026</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[startup]]></category><dc:creator><![CDATA[Francisco Luna]]></dc:creator><pubDate>Sun, 07 Jun 2026 23:26:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/689f4f0d610919fe10ba3c12/1f735f98-b6ed-4de0-94a0-49d35e7dfeb2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently at work, we've had to deal with a business pivot, yet, again. I wasn't happy to hear this after carefully planning the first version of the app, but my intuition told me it was going to happen, which is why I worked to keep the system's architecture flexible. Let me introduce you to the core problem, how I've been solving it, and how you could prevent similar things from happening in the future.</p>
<h2>1. The business change</h2>
<h3>The first prototype (V1) - January and May 2026</h3>
<p>In V1, we were working with separate locations (tenants) in our app. It was a traditional multi-tenant architecture in a granular way, as the requirements of January 2026 suggested. My CEO's words:</p>
<ul>
<li><p>Location is the primary unit of experience.</p>
</li>
<li><p>Pricing is per location.</p>
</li>
<li><p>Users interact in a location.</p>
</li>
<li><p>Posts and chats are never shared across locations.</p>
</li>
<li><p>Brands are labels only; locations are the product.</p>
</li>
<li><p>Do NOT merge locations into one shared feed."</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/689f4f0d610919fe10ba3c12/489ea201-f03d-41e1-be5a-55c0c757f37d.png" alt="" style="display:block;margin:0 auto" />

<h3>The pivot (V2) on May - June 2026</h3>
<p>Weeks after launch, on May 29, I received a suggestion from my CEO. "Fran, we've noticed the current business model won't scale, and the logistics are hard to manage. Let's merge the related tenants to brands and create a global app per brand". My mistake? I pretty much married everything to the local tenant_id as shown below:</p>
<img src="https://cdn.hashnode.com/uploads/covers/689f4f0d610919fe10ba3c12/f20a9a9c-139f-4b28-9362-2b387a6e9066.png" alt="" style="display:block;margin:0 auto" />

<p>What I did not know was that each tenant needed to belong to a brand as well. Essentially allowing brands to be the main entity, not the location.</p>
<p>I ended up adapting to the change because the database schemas were flexible and I understood the codebase well. The reasoning behind this change is related to profit. We found out that brands can give the startup the most revenue, while individual locations were not profitable for us. We're working on migrating the core app architecture to work with brands.</p>
<h2>2. The strangler fig pattern</h2>
<p>The strangler fig pattern is a software architecture pattern where you create a new system alongside the old one, and eventually the new system deprecates the old one due to the new changes in the core functionality. With this migration, we've done the following:</p>
<ul>
<li><p>We focused on building the new admin features related to brands, as in a B2B app, you always need to maintain two types of systems: the customer-facing application and the administrative one (who creates the main system information, deals with reports, user management, and sees the metrics? Correct, the system administrators; now you know it).</p>
</li>
<li><p>We've kept the core user authentication almost the same and only extended a few API endpoints to retrieve new fields or support brands without major changes in the core logic.</p>
</li>
<li><p>For the new brand and social features, we've created a V2 of the API where the main logic relies on the brand, not on the location as we were doing before.</p>
</li>
<li><p>We introduced graceful degradation strategies in the frontend, where users are now redirected to the brand page once they are migrated from a location to a brand, keeping a seamless UX/UI.</p>
</li>
</ul>
<h2>3. Don't add everything they ask you for at once</h2>
<p>The team and I were asked to introduce the following as well:</p>
<ol>
<li><p>Filters and being able to search users by brand</p>
</li>
<li><p>User location preferences Geolocation and extending existing locations</p>
</li>
</ol>
<p>I added these features to the backlog as they were pure scope creep. The main goal right now for the team is to finish the business model change and ensure existing users can use the new, polished app.</p>
<p>Once we've shipped this new system and we've accomplished the given business goals, we can keep enhancing the existing foundation we built. You cannot build on broken foundations.</p>
<h2>4. Continuing the migration</h2>
<p>Besides the strategies I mentioned in the Strangler Fig pattern section, there are other strategies I'm going to apply.</p>
<ul>
<li><p>Add the brand_id to the social-related tables and make them nullable while we perform the migration process, and stop using location_id entirely in the new API to match the new business context.</p>
</li>
<li><p>Migrate users from locations to brands using a membership table, where we also had the location_id. We migrate our users both in-app through endpoints like "/me" and using a startup script triggered by a feature flag.</p>
</li>
<li><p>Don't make new columns non-nullable until the migration process is done, and don't remove the location_id yet until the new migration strategy works for 100% of users. We always need backwards compatibility in case of a disaster or a wrong business plan.</p>
</li>
</ul>
<h2>5. Lessons learned &amp; conclusion</h2>
<ul>
<li><p>Multi-tenancy is hard. You won't get it right at the first attempt, and even if you do so, you'll have to deal with RLS (Row Level Security) and complex RBAC permissions.</p>
</li>
<li><p>Keep your schemas and architecture flexible in the initial startup stage. There will be pivots, and your code needs to adapt to the business, not the other way around.</p>
</li>
<li><p>Learn how to learn from pivots and changes in business directions. You're getting paid to solve problems; some of them are interesting, others are not so fun, but you still have to find meaning during the process.</p>
</li>
</ul>
<p>I've been improvising lately on the electric guitar, where you have to adapt your skills to others' music; the same happens with startups, adapt your skills to the business, not the other way around. Thanks for reading, I'll continue working on this challenge, and I hope you have learned something new :)</p>
]]></content:encoded></item><item><title><![CDATA[The Best Code is Unwritten: Architectural Lessons from Startup Production (May 2026)]]></title><description><![CDATA[I started building for startups in 2023. By 2026, I was migrating live auth systems, cutting query latency by 79%, and learning that the hardest part of engineering isn’t shipping code, it’s deciding ]]></description><link>https://blog.itsfranciscoluna.com/the-best-code-is-unwritten-architectural-lessons-from-startup-production-may-2026</link><guid isPermaLink="true">https://blog.itsfranciscoluna.com/the-best-code-is-unwritten-architectural-lessons-from-startup-production-may-2026</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[startup]]></category><dc:creator><![CDATA[Francisco Luna]]></dc:creator><pubDate>Wed, 27 May 2026 20:05:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/689f4f0d610919fe10ba3c12/a420eba5-e8df-4d5a-a3d6-197ae4328d4f.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I started building for startups in 2023. By 2026, I was migrating live auth systems, cutting query latency by 79%, and learning that the hardest part of engineering isn’t shipping code, it’s deciding what code should not be written.</p>
<p>This article is a compilation of practical architectural lessons and counterintuitive insights I’ve gathered in the trenches this year, up to May 25, 2026.</p>
<h2>Don't reinvent the wheel with auth</h2>
<p>I still remember the time when I finished writing the main auth implementation for our main platform at work. I was proud of myself for using custom JWTs and refresh token mechanisms, and for spending hours building the authentication feature.</p>
<p>However, days later, after launching our MVP, I got a new requirement:</p>
<p>"Now the system must be able to support OAuth with Google."</p>
<p>How was I able to tell my CEO that our authentication system was held together with duct tape and prayers? I decided to migrate the authentication system from a custom JWT implementation to a dedicated library and migrate the stateless auth to sessions.</p>
<p>You can implement auth in 3 different ways in a system:</p>
<ol>
<li><p>Building it yourself and dealing with HIPAA, CSRF, refresh tokens, stateless vs stateful auth, whatnot. You'll be spending more time building and testing this than shipping features.</p>
</li>
<li><p>Using dedicated libraries to build it. Such as BetterAuth, Passport, Laravel Sanctum, etc. It prevents vendor lock-in; however, you must use a secure and battle-tested option.</p>
</li>
<li><p>Using services like Auth0 and Clerk. While this option is valid for solopreneurs or side projects, companies and organizations are skeptical about sharing their information and user base with a third-party provider.</p>
</li>
</ol>
<p>If you’re thinking, "I’ll just build it myself," please don’t. Unless you have built authentication systems at a corporate level from scratch and you work closely with a security engineer, you'll struggle with ALL the edge cases during the process.</p>
<h2>Don't rewrite, embrace the system</h2>
<p>An overengineered system working in production is better than a rewrite.</p>
<p>I was talking with a client about a rewrite because we're automating too many things in our CI/CD pipeline, and we have OpenAPI 3.1 documentation for everything. Yes, it's kind of overkill for a small product, but it'll allow us to go faster with the time.</p>
<p>We're now focused on delivering features and scaling the system. Migrating our decoupled architecture to a monolith is a task with no benefits because we're still finding PMF, and our system works with 99.995% uptime. At the end, we only rewrote the authentication module for compliance purposes.</p>
<p>Yes, the developer experience is just good, but the team and I know the trade-offs made and where the system bottlenecks are. It's not worth doing a rewrite when you're finding PMF, you're a small team, and there are no clear benefits from it.</p>
<h2>Write tests for every bug you fix</h2>
<p>A stakeholder asked me why users could not register again in the app once they deleted their account. I verified the database schema, and it confirmed we had a unique constraint for the user email. I removed it, since we're dealing with soft deletes in a multi-tenant system.</p>
<p>I ran the tests; 6 verifications and checks broke in the system since we had not tested this rare edge case that our stakeholder found. I fixed the use cases and wrote a new test to verify that this specific edge case never happens again. All tests were green again.</p>
<h2>Embrace incremental migrations</h2>
<p>Eventually, you'll have to learn this lesson. Avoid running scripts or commands to make database changes in a production system.</p>
<p>Use incremental migration instead of using cron jobs or when the user performs a specific system operation.</p>
<p>Let's say you need to rewrite the authentication system to use accounts.</p>
<p>Instead of migrating the entire user data and passwords to the new tables, you can do the following:</p>
<ul>
<li><p>Whenever a new user signs in, you create an account and link their existing password and authentication method to the new table. This allowed us to migrate 23% of our existing user base in a single day.</p>
</li>
<li><p>Write tests to verify legacy users can sign in properly. You should not modify the hashing algorithm and system internals used to perform this process, only the way you're retrieving and saving data.</p>
</li>
<li><p>You can use dual-writes, but research the trade-offs and see if they fit your system requirements.</p>
</li>
<li><p>The sign-up method is implemented using the new logic because it is a fresh write operation.</p>
</li>
</ul>
<h2>Communicate well with stakeholders</h2>
<p>Here are a few rules to communicate with your non-technical manager and stakeholders:</p>
<ul>
<li><p>They care about results and deadlines. Speak using results and simple language. (e.g., 'We’re updating how we store deleted users so reactivation doesn’t fail. ETA: Thursday. ' don't say things like 'We need to refactor the DB schema. ')</p>
</li>
<li><p>Learn how to say no without using the word "no". (e.g., "We can build a mobile app in the future, but for now, let's focus on getting 200 DAU and converting 8% of them to our premium plan)</p>
</li>
<li><p>State the why, not the how (e.g., "I fixed the bug to upload images, now the onboarding step will convert more users")</p>
</li>
<li><p>Master Git, conventional commits, and technical writing/documentation. Even if you work alone, you need to be able to analyze your own work for future reports.</p>
</li>
<li><p>Document your work consistently. Not for a promotion, for mastery.</p>
</li>
</ul>
<h2>Building is easy, maintaining is not</h2>
<p>How I wish we could go back to building the same system again using Next.js/Laravel and keep things simple.</p>
<p>Yet actual users and businesses now rely on the platform we built using decoupled architecture. This means we introduced DevOps and development overhead using a separate backend and frontend, but now we can build for scale.</p>
<p>My advice here is to maintain a project, any kind of software development project. For months and even years, if you can. It's going to teach a lot about your tech stack's weaknesses, how to pay technical debt, deal with legacy code, and how actual users interact with the application.</p>
<p>Underrated skills you need in a startup</p>
<p>Forget about React, K8s, Java/Spring, or microservices for a second. I recommend learning this well before working in a startup:</p>
<ul>
<li><p>IaC (Pulumi, Terraform - you need new and existing environments to be replicated using code)</p>
</li>
<li><p>A framework to build monolithic applications (Laravel, Next.js, Django - microservices and decoupled architectures are not everything)</p>
</li>
<li><p>DevOps (automating deployments - so you can focus on building)</p>
</li>
<li><p>How to create technical documentation for your work (you can have a document of achievements in Google Docs or Notion)</p>
</li>
<li><p>Linux, SQL, and PostgreSQL (2am emergencies, restoring a database from a backup...)</p>
</li>
<li><p>Testing (unit tests, E2E tests, and integration tests - Playwright, Vitest, and similar frameworks).</p>
</li>
<li><p>Git, conventional commits, branches, PRs, merges, and how to use version control well.</p>
</li>
</ul>
<h2>Conclusion</h2>
<p>Software engineering is not about finding the perfect architecture; it is about choosing which trade-offs you are willing to live with.</p>
<p>None of the insights shared here are dogmatic rules. Every startup faces unique bottlenecks, and the stack or strategy you choose must strictly serve your current business landscape.</p>
<p>However, if there is one universal truth I’ve learned from maintaining systems in production, it’s that the tools you use matter far less than your discipline. You need to focus on building systems that allow you to sleep at night while production runs seamlessly.</p>
<p>Choose your technical debt wisely, document your decisions, and play the long game.</p>
]]></content:encoded></item><item><title><![CDATA[Startup Engineering Playbook]]></title><description><![CDATA[Sixteen months of work, a failed product, and a near-departure from engineering. I used to think software engineering was about writing fancy code and talking to stakeholders. I was wrong.
This took m]]></description><link>https://blog.itsfranciscoluna.com/startup-engineering-playbook</link><guid isPermaLink="true">https://blog.itsfranciscoluna.com/startup-engineering-playbook</guid><dc:creator><![CDATA[Francisco Luna]]></dc:creator><pubDate>Fri, 27 Feb 2026 15:52:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/689f4f0d610919fe10ba3c12/6b1d0e23-f013-4879-935a-090fa274dd02.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sixteen months of work, a failed product, and a near-departure from engineering. I used to think software engineering was about writing fancy code and talking to stakeholders. I was wrong.</p>
<p>This took me almost 2 months of hiatus reading books, writing post morterms and learning from my past mistakes. Let me share with you what's important in tech startups:</p>
<h2>Architecture matters</h2>
<p>In previous projects, I ended up with technical debt that was impossible to paid without a major rewrite and I had either premature or wrong abstractions. In any serious software project that you have to maintain over time, you need to consider using a serious architecture to keep consistency.</p>
<p>With software architecture, I'm not meaning to the folder structure where you separate your code by services, controllers and views. You can separate your code by folders and it can still create cognitive overhead because of god objects and nonexistent abstractions.</p>
<p>Cognitive overhead is avoided isolating responsibilities. Use cases do one functionality well and you use ports and adapters to make your code like lego blocks.</p>
<p>However, architecture is a medium, not the end. You don't need to follow all clean code standards perfectly, just ensure your team has standards, quality assurance (QA) and you can adapt to the change of requirements.</p>
<h2>Kill scope creep</h2>
<p>I learned how to spot scope creep after falling for it <strong>several</strong> times. Scope creep refers to any features you believe help the product before you've validated your idea well, even if you don't have proof.</p>
<p>Common examples are:</p>
<ul>
<li><p>Search inputs in feeds with less than 100 total users (but you can find everything in a single page...)</p>
</li>
<li><p>Filters and multiple categories in a social media feed MVP (are the people even posting there?)</p>
</li>
<li><p>Combining multiple app functionalities in a single one (is this a social media app, a habits tracker or what?)</p>
</li>
<li><p>Features that consider paranoid edge cases (let's add multiple timezones because of... let's send the user a message in 5 different ways...)</p>
</li>
</ul>
<p>I've been telling this to other startup founders and entrepeneurs. First, validate your core feature and get users. Later, if users ask for these features, we can add them.</p>
<p>Something else happens when you don't cut off scope creep:</p>
<p><strong>Your product gets more complex to use and your users stop using the app.</strong></p>
<p>This note itself has its worth in gold. Keep your products and ideas simple, add new features and functionalities only when other people ask for it.</p>
<h2>Question everything</h2>
<p>Most people have labeled me as a rebel or paranoid. In business this is a blessing, because when you don't question an idea, you build without a plan and blindly. You're being reactive, not proactive.</p>
<p>Always make sure to ask these questions to your team:</p>
<ul>
<li><p>When is the actual deadline for this project? (always be asking this, it might sound relevant but PMs and non-technical people change their minds quite often)</p>
</li>
<li><p>Why is this needed and what do you expect from this project or functionality?</p>
</li>
<li><p>How is this feature helping users and why is it important?</p>
</li>
<li><p>How are we different from the competition and why is people going to use our products?</p>
</li>
<li><p>Do we need this feature or are we building it just in case?</p>
</li>
</ul>
<p>Also question your architecture choices often:</p>
<ul>
<li><p>Do I need to add Websockets here when I can use server sent events?</p>
</li>
<li><p>Is Redis needed here or am I just thinking about a fictional scenario?</p>
</li>
<li><p>Am I normalizing my schemas for the sake of it or is this helping the business?</p>
</li>
<li><p>Why am I even doing optimistic updates when I can just make a new API call for MVP?</p>
</li>
</ul>
<h2>Solve your own problems first</h2>
<p>I used to hate sending invoices because I was doing the whole process by hand.</p>
<p>I automated it using n8n, Google Docs, Gmail API and JavaScript in a weekend, now it saves me 2 hours monthly.</p>
<p>To be honest I also dislike sending weekly engineering reports by hand because it's all on the Git history but someone has to translate it to stakeholders.</p>
<p>So I built another automation. It gets the git history of a repository and turns it into business results. I tweak it and I have a meeting agenda.</p>
<p>These are for my business and they're humble integrations that solved actual problems I had. my message here is simple:</p>
<p><strong>Fix problems for yourself before solving them for others</strong></p>
<p>How can you guide and support other people if you're not able to support yourself first? You need to be willing to learn from your mistakes and find ways to improve yourself.</p>
<p>Maybe your next business idea is an app you build for yourself first.</p>
<p>Think about it :)</p>
<h2>Choose your path</h2>
<p>I tried to get into FAANG and I was chasing it, but for what really? Money and reputation? That's not what I want right now.</p>
<p>I promised myself I was not going to compete with other engineers to land a fancy job.</p>
<p>I've preferred to work with non-technical founders, dive deeper into backend engineering, automation and the business side of things. So far, it's worked well and we've shipped great software.</p>
<p>To some people I'm a mentor, for others a founding engineer, and some people don't even know what I do.</p>
<p>This is what I do. I help founders build startups, digital products and I educate others about software engineering and architecture.</p>
<p>It's okay if you want to land a job in a startup.</p>
<p>It's okay if you want to get into FAANG.</p>
<p>It's okay if you want Series C funding.</p>
<p>It's okay if you want to build an app and share it with other people.</p>
<p>As long as you're aware of your strengths, weaknesses, personality traits and understand there won't be an easy path, you'll make progress in your journey.</p>
<h2>General Advice</h2>
<ul>
<li><p>Give time buffers. If a project is estimated to take you 3 months, explicitly mention it'll be 5 months. You don't know what might change or how life develops.</p>
</li>
<li><p>If you're consulting, greenfield projects are charged by milestones, not by the hour. It's more beneficial for you and your employer.</p>
</li>
<li><p>Surround yourself with great people who are willing to improve daily.</p>
</li>
<li><p>Don't take failure personally, study why things failed, learn to never do so again and move on.</p>
</li>
<li><p>Don't fix the symptom of a problem, fix the root of it. This applies to everything in life.</p>
</li>
<li><p>Learn new things outside work, read and experiment with new technologies. Your main position or gig can burn you out if you don't introduce variation in your life.</p>
</li>
<li><p>Always be willing to have tough talks with your people. Be clear and honest about your perspective and long term thinking.</p>
</li>
<li><p>Make your codebase and configuration as flexible as possible in startups, your mental health will thank you.</p>
</li>
<li><p>Your goal as an entrepreneur is to help more people.</p>
</li>
<li><p>It's better to spend 1 week planning than 1 year building mindlessly.</p>
</li>
<li><p>Remember, entrepeneurship is the medium, not the end.</p>
</li>
</ul>
<hr />
<p>Thanks for reading! I hope you've learned something new here. You can also learn more about my projects in my website: <a href="https://www.itsfranciscoluna.com/">https://www.itsfranciscoluna.com/</a></p>
]]></content:encoded></item><item><title><![CDATA[Surprising Backend Tips Revealed After 2 Years of Real-World Experience]]></title><description><![CDATA[When I started working on backend systems I didn’t know what I was doing. I was there repeating logic, building fragile environments, chasing the next trendy framework and overcomplicated things that didn’t need to be complex. Most of the pain came f...]]></description><link>https://blog.itsfranciscoluna.com/surprising-backend-tips-revealed-after-2-years-of-real-world-experience</link><guid isPermaLink="true">https://blog.itsfranciscoluna.com/surprising-backend-tips-revealed-after-2-years-of-real-world-experience</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[backend]]></category><category><![CDATA[Databases]]></category><category><![CDATA[data structures]]></category><category><![CDATA[Career]]></category><dc:creator><![CDATA[Francisco Luna]]></dc:creator><pubDate>Wed, 03 Sep 2025 22:27:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/aLhce55lLPk/upload/6786f58ec5f1be7a753b2366a05d1869.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I started working on backend systems I didn’t know what I was doing. I was there repeating logic, building fragile environments, chasing the next trendy framework and overcomplicated things that didn’t need to be complex. Most of the pain came from missing the fundamentals. Things no one really teaches until you’ve broken enough code to learn them yourself.</p>
<p>This blog is a collection of lessons I wish I had earlier. It’s not about being perfect but about building systems that are clean, scalable, and maintainable in real-world conditions. Whether you’re solo or in a small team, these practices will help you ship faster, debug less, and build with confidence. Let’s get started.</p>
<h2 id="heading-leverage-openapi-and-documentation-standards">📘 Leverage OpenAPI and Documentation Standards</h2>
<p>OpenAPI has become the standard specification for modern APIs. It allows you to document your endpoints, DTOs, responses, and error codes in a structured, scalable way. Once defined, your specification can be visualized using open-source tools like <strong>SwaggerUI</strong> or <strong>Scalar</strong>, giving your team and clients a clear view of your API’s surface.</p>
<p>Implementation might be different across codebases. Some teams still write specs manually, which I don’t recommend. Others use tools like <code>zod-to-openapi</code> or framework-native solutions to generate specs directly from validation schemas and DTOs. Personally I prefer a <strong>programmatic approach</strong>, keeping the spec in sync with the codebase ensures accuracy and reduces friction.</p>
<h3 id="heading-what-about-the-client-common-pitfalls-amp-solutions">💬 What About the Client? Common Pitfalls &amp; Solutions</h3>
<p>So far, we’ve focused on backend documentation. You might visit your generated docs at:</p>
<pre><code class="lang-markdown">http://localhost:3000/docs
</code></pre>
<p>…and see a beautifully rendered list of endpoints. But if you’re a <strong>Full Stack Engineer</strong>, a <strong>solo dev</strong>, or part of a <strong>frontend team</strong>, manually recreating every type and contract on the client side is a recipe for inconsistency. Even with AI assistance, it doesn’t scale; especially when schemas evolve.</p>
<p>That’s why I recommend using libraries like <strong>OpenAPI TypeScript</strong>. These tools generate <strong>type-safe clients</strong> directly from your OpenAPI spec, ensuring your frontend stays aligned with your backend without redundant effort. In our mono-repo setup, this approach has been a game-changer.</p>
<h2 id="heading-separate-business-logic">🧠 Separate Business Logic</h2>
<p>This isn’t a blog about hexagonal architecture, clean architecture, or MVC and I’m not here to tell you which one to follow. What matters is <strong>separation</strong>. You need to understand how to structure your application so that logic isn’t tangled in a single file or scattered across untraceable layers.</p>
<h3 id="heading-what-separation-actually-means">✨ What Separation Actually Means</h3>
<ul>
<li><p><strong>Repositories</strong> → Handle data access and persistence</p>
</li>
<li><p><strong>Controllers</strong> → Receive requests, delegate to services</p>
</li>
<li><p><strong>Services</strong> → Contain business logic, orchestrate flows</p>
</li>
<li><p><strong>Infrastructure Layers</strong> → External systems (e.g., DB, cache, queues)</p>
</li>
<li><p><strong>DTOs</strong> → Define shape and contract of data</p>
</li>
<li><p><strong>Entities / Tables</strong> → Define how the core business model or entity will be created</p>
</li>
<li><p><strong>Routes</strong> → Map endpoints to controllers</p>
</li>
<li><p><strong>Validations</strong> → Ensure data integrity before logic runs</p>
</li>
<li><p><strong>Types</strong> → Enforce clarity across the stack</p>
</li>
<li><p><strong>Dependency Injection / Containers</strong> → Manage lifecycle and initialization</p>
</li>
</ul>
<h3 id="heading-what-to-avoid">❌ What to avoid</h3>
<p>I’ve seen this countless of times. It’s okay for prototypes and quick iterations but please, don’t come up with this in a project that will scale:</p>
<pre><code class="lang-typescript">app.post(<span class="hljs-string">'/create-user'</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">const</span> { name, email } = req.body;
  <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> db.insert({ name, email });
  res.json(user);
});
</code></pre>
<h3 id="heading-what-to-aim-for">✅ What to aim for</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// route.ts</span>
router.post(<span class="hljs-string">'/create-user'</span>, userController.createUser);

<span class="hljs-comment">// controller.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createUser = <span class="hljs-keyword">async</span> (req, res) =&gt; {
  <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> userService.create(req.body);
  res.json(user);
};

<span class="hljs-comment">// service.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> create = <span class="hljs-keyword">async</span> (data) =&gt; {
  validateUser(data);
  <span class="hljs-keyword">return</span> userRepository.insert(data);
};
</code></pre>
<h3 id="heading-folder-structure-and-architecture">📁 Folder Structure and Architecture</h3>
<p>You don’t need to follow a single architectural dogma. Whether it’s <strong>Clean Architecture</strong>, <strong>DDD</strong>, <strong>Screaming Architecture</strong>, or a hybrid, what matters is that your structure <strong>scales</strong> and your team can <strong>read it without friction</strong>.</p>
<p>Personally, I lean toward a layered pattern that balances clarity and modularity:</p>
<ul>
<li><p><strong>API Layer</strong> → Routes, Controllers, Schemas, Services, Repositories, Validations, Entities</p>
</li>
<li><p><strong>Global Layer</strong> → Shared infrastructure (AWS SDKs, Sentry, Stripe, etc.), types, utilities, and specs</p>
</li>
</ul>
<h2 id="heading-document-everything">📋 Document Everything</h2>
<p>In the codebase I’m currently working on, there are several non-obvious configurations that, if undocumented, would lead to significant friction and confusion. For example, due to limitations with ESM modules and specific ORM behaviors, we must <strong>compile the backend application before executing database migrations</strong>. Additionally, our seeding process involves junction tables that are essential for scalability, and each project within the monorepo includes its own <code>.env.example</code> file to manage environment variables.</p>
<p>Without clear documentation of:</p>
<ul>
<li><p>The ESM module constraints impacting migration execution.</p>
</li>
<li><p>The seeding logic required to initialize the application correctly.</p>
</li>
<li><p>The environment variables distributed across modules.</p>
</li>
<li><p>The onboarding experience would be chaotic, and maintaining the system would become unnecessarily burdensome.</p>
</li>
</ul>
<h3 id="heading-best-practices">✨ Best Practices</h3>
<ul>
<li><p>If you uncover a cryptic configuration, <strong>document it</strong>.</p>
</li>
<li><p>If you discover a way to improve CI/CD, <strong>share it and document it</strong>.</p>
</li>
<li><p>If you encounter a bug that only you understand, <strong>fix it and document it immediately</strong>.</p>
</li>
</ul>
<h2 id="heading-dry-and-design-patterns">🎹 DRY and Design Patterns</h2>
<p>When I first joined this backend codebase, I made a classic mistake. I created a separate repository for every single entity, even when their logic was nearly identical…</p>
<p>For example, we had tables for <strong>daily</strong>, <strong>weekly</strong>, and <strong>monthly</strong> tasks, and I wrote three distinct repositories with almost the same CRUD operations:</p>
<p>Technically, this worked. But it introduced a long-term problem: Every schema change, every refactor, every new feature had to be triplicated. It was fragile, redundant, and emotionally exhausting.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// dailyTask.repository.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createDailyTask = <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> db.dailyTask.create(data);

<span class="hljs-comment">// weeklyTask.repository.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createWeeklyTask = <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> db.weeklyTask.create(data);

<span class="hljs-comment">// monthlyTask.repository.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createMonthlyTask = <span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> db.monthlyTask.create(data);
</code></pre>
<h3 id="heading-refactoring-with-dry-principles">📚 Refactoring with DRY Principles</h3>
<p>To resolve this, I consolidated the logic into a <strong>generic task repository</strong> using abstraction and shared patterns:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// task.repository.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createTask = <span class="hljs-function">(<span class="hljs-params"><span class="hljs-keyword">type</span>: <span class="hljs-string">'daily'</span> | <span class="hljs-string">'weekly'</span> | <span class="hljs-string">'monthly'</span>, data</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> db[<span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-keyword">type</span>}</span>Task`</span>].create(data);
};
</code></pre>
<p>You can apply guard clauses, strategy patterns, or other abstractions depending on your context. The key is to <strong>avoid duplicating logic</strong> that behaves the same across entities.</p>
<h3 id="heading-benefits-of-this-approach">✅ Benefits of This Approach</h3>
<ul>
<li><p><strong>Reduces duplication</strong></p>
</li>
<li><p><strong>Centralizes logic</strong></p>
</li>
<li><p><strong>Simplifies future changes</strong></p>
</li>
<li><p><strong>Improves testability and readability</strong></p>
</li>
<li><p><strong>Honors the DRY principle</strong></p>
</li>
</ul>
<h2 id="heading-kiss-dont-overcomplicate-things">🧠 KISS – Don’t Overcomplicate Things</h2>
<p>No. You don’t need an <code>AbstractClassAppInitializationFactory</code> when you’re not even in the market yet.</p>
<p>The purpose of an MVP (Minimum Viable Product) or MLP (Minimum Lovable Product) is to deliver something of value to your initial users. <strong>KISS is not an excuse to ignore best practices</strong>. You <strong>SHOULD</strong> still use reusable patterns, structure your code, apply Dependency Injection, and follow clean conventions. But complexity should be introduced <strong>only when it’s necessary</strong>.</p>
<p>You can scale with Redis. You can scale with Nest.js. You can scale with a monolith that’s well-structured.</p>
<p>But if you’re building abstract design patterns that delay delivery just to sound smart you’re not building a product. You’re digging your own grave. Especially if your team is small or if you’re solo.</p>
<h3 id="heading-bad-too-much-initial-complexity">❌ Bad - Too Much Initial Complexity</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">interface</span> PostDeletionStrategy {
  execute(postId: <span class="hljs-built_in">string</span>, userId: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt;;
}

<span class="hljs-keyword">class</span> DeletePostHandler <span class="hljs-keyword">implements</span> PostDeletionStrategy {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">
    <span class="hljs-keyword">private</span> repo: {
      getById: (id: <span class="hljs-built_in">string</span>) =&gt; <span class="hljs-built_in">Promise</span>&lt;{ userId: <span class="hljs-built_in">string</span> } | <span class="hljs-literal">null</span>&gt;;
      deleteById: (id: <span class="hljs-built_in">string</span>) =&gt; <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt;;
    },
    <span class="hljs-keyword">private</span> auth: {
      isAuthorized: (userId: <span class="hljs-built_in">string</span>, ownerId: <span class="hljs-built_in">string</span>) =&gt; <span class="hljs-built_in">boolean</span>;
    }
  </span>) {}

  <span class="hljs-keyword">async</span> execute(postId: <span class="hljs-built_in">string</span>, userId: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">const</span> post = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.repo.getById(postId);
    <span class="hljs-keyword">if</span> (!post) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'PostNotFound'</span>);
    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.auth.isAuthorized(userId, post.userId)) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Forbidden'</span>);
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.repo.deleteById(postId);
  }
}

<span class="hljs-keyword">class</span> PostService {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">private</span> deletion: PostDeletionStrategy</span>) {}

  <span class="hljs-keyword">async</span> <span class="hljs-keyword">delete</span>(postId: <span class="hljs-built_in">string</span>, userId: <span class="hljs-built_in">string</span>) {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.deletion.execute(postId, userId);
  }
}

<span class="hljs-keyword">const</span> service = <span class="hljs-keyword">new</span> PostService(<span class="hljs-keyword">new</span> DeletePostHandler(repo, auth));
<span class="hljs-keyword">await</span> service.delete(<span class="hljs-string">'post123'</span>, <span class="hljs-string">'user456'</span>);
</code></pre>
<p>This works but there’s too much complex logic for a single operations. You’ve introduced interfaces, base handlers, and strategy patterns before even validating the product…</p>
<h3 id="heading-the-fix-just-go-to-market-with-good-practices">✅ The Fix: Just Go to Market with Good Practices</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">type</span> Dependencies = {
  repo: {
    getById: <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;Post | <span class="hljs-literal">null</span>&gt;;
    deleteById: <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt;;
  };
};

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PostService</span>(<span class="hljs-params">{ repo }: Dependencies</span>) </span>{
  <span class="hljs-keyword">return</span> {
    deleteById: <span class="hljs-keyword">async</span> (id: <span class="hljs-built_in">string</span>, userId: <span class="hljs-built_in">string</span>) =&gt; {
      <span class="hljs-keyword">const</span> post = <span class="hljs-keyword">await</span> repo.getById(id);
      <span class="hljs-keyword">if</span> (!post) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Not found'</span>);
      <span class="hljs-keyword">if</span> (post.userId !== userId) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Forbidden'</span>);
      <span class="hljs-keyword">await</span> repo.deleteById(id);
    },
  };
}
</code></pre>
<p>This version is clean, testable, and scalable. It honors separation of concerns without being complex. It gets you to market faster and will allow you to work on other features as well.</p>
<h2 id="heading-docker-real-life-development-environments">🐳 Docker - Real Life Development Environments</h2>
<p>A lot of people ask me why I love Docker. One year ago, a client asked me to run a project for them. They cloned the GitHub repo, followed the installation instructions, and then… <strong>CHAOS</strong>.</p>
<p>They had to manually set up PostgreSQL, Redis, Node.js, PNPM, Next.js, run database migrations, configure Stripe and other external integrations... It was one of the most awkward dev experiences I’ve ever had. Months later, I learned Docker and Docker Compose, and it changed everything. I now save 8+ hours a week by avoiding manual setups, Linux environment configs, and repetitive integration commands. Everything’s dockerized and automated in my dev environment.</p>
<p><strong>Do yourself a favor: leverage automation scripts in your IDE, learn Docker and containerization, and document every environment variable in a</strong> <code>.env.example</code> <strong>file.</strong></p>
<p>You don’t want to go through what I did. <strong>TRUST ME.</strong></p>
<h2 id="heading-clear-api-responses">🧾 Clear API Responses</h2>
<p>Clear API responses matter. Don’t manually return JSON objects in every controller. You’ll forget field names, break consistency, and end up with endpoints that return <code>"info"</code> instead of <code>"data"</code> because you got creative.</p>
<p>Instead, build a simple response adapter. It can be just a dictionary of methods that wrap your response logic. This way, every success, error, or edge case goes through the same structure. You define it once and call it everywhere. It’s not fancy, it’s just maintainable. Your future self will thank you when debugging a 409 Conflict at scale doesn’t feel like a Dark Souls bossfight. Keep it clean. Keep it predictable.</p>
<h3 id="heading-bad-practice-manually-creating-the-response">❌ Bad Practice - Manually Creating the Response</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).json({
success: <span class="hljs-literal">true</span>,
data: someData
statusCode: <span class="hljs-number">200</span>
})
</code></pre>
<h3 id="heading-good-practice-using-an-adapter">✅ Good Practice - Using an Adapter</h3>
<p>An adapter is a artifact which you can use to turn data from external systems into your own. Ideal for API responses and data transformation such as the example down below!</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> ApiResponse = {
  success: &lt;T&gt;<span class="hljs-function">(<span class="hljs-params">res: Response, data?: T, message = <span class="hljs-string">'Success'</span></span>) =&gt;</span>
    res.status(<span class="hljs-number">200</span>).json({ statusCode: <span class="hljs-number">200</span>, message, data }),

  created: &lt;T&gt;<span class="hljs-function">(<span class="hljs-params">res: Response, data?: T, message = <span class="hljs-string">'Created'</span></span>) =&gt;</span>
    res.status(<span class="hljs-number">201</span>).json({ statusCode: <span class="hljs-number">201</span>, message, data }),

  error: <span class="hljs-function">(<span class="hljs-params">res: Response, message = <span class="hljs-string">'Error'</span>, statusCode = <span class="hljs-number">500</span>, error?: <span class="hljs-built_in">any</span></span>) =&gt;</span>
    res.status(statusCode).json({ statusCode, message, error }),

  notFound: <span class="hljs-function">(<span class="hljs-params">res: Response, message = <span class="hljs-string">'Not Found'</span></span>) =&gt;</span>
    res.status(<span class="hljs-number">404</span>).json({ statusCode: <span class="hljs-number">404</span>, message }),

  badRequest: <span class="hljs-function">(<span class="hljs-params">res: Response, message = <span class="hljs-string">'Bad Request'</span>, error?: <span class="hljs-built_in">any</span></span>) =&gt;</span>
    res.status(<span class="hljs-number">400</span>).json({ statusCode: <span class="hljs-number">400</span>, message, error }),
};
</code></pre>
<p>Which you can use like this in a controller now!</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">return</span> ApiResponse.success(res, serviceResponse, <span class="hljs-string">'Successfully fetched the user.'</span>)
</code></pre>
<h2 id="heading-leverage-cache">🔥 Leverage Cache</h2>
<p>Why does cache even matter? Let’s talk about how we use junction tables in an application. When setting up the app in a new environment, we need to seed the database with categories, filters, and metadata. It’s not fun, but it’s the most scalable way to keep things normalized. We tried using JSON blobs and <code>string[]</code> arrays, but they didn’t scale well. So we built a proper repository and added cache.</p>
<p>We constantly query for things like interests and categories. These don’t change often, maybe once every few months. But we were still hitting the database every time. That works with 100 users. Not with 10,000. It’s a waste of resources at scale.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> TOPICS_CACHE_KEY = <span class="hljs-string">'topics:all'</span>;
<span class="hljs-keyword">const</span> CACHE_TTL = <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">24</span>; <span class="hljs-comment">// 24 hours</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> UserTopicsRepository = <span class="hljs-function">(<span class="hljs-params">db: <span class="hljs-built_in">any</span>, redis: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> getTopics = <span class="hljs-keyword">async</span> (): <span class="hljs-built_in">Promise</span>&lt;{ id: <span class="hljs-built_in">string</span>; slug: <span class="hljs-built_in">string</span> }[]&gt; =&gt; {
    <span class="hljs-keyword">const</span> cached = <span class="hljs-keyword">await</span> redis.get(TOPICS_CACHE_KEY);
    <span class="hljs-keyword">if</span> (cached) <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(cached);

    <span class="hljs-keyword">const</span> topics = <span class="hljs-keyword">await</span> db.select().from(<span class="hljs-string">'Topics'</span>);
    <span class="hljs-keyword">await</span> redis.set(TOPICS_CACHE_KEY, <span class="hljs-built_in">JSON</span>.stringify(topics), { EX: CACHE_TTL });
    <span class="hljs-keyword">return</span> topics;
  };

  <span class="hljs-keyword">const</span> syncUserTopics = <span class="hljs-keyword">async</span> (userId: <span class="hljs-built_in">string</span>, topicSlugs: <span class="hljs-built_in">string</span>[]) =&gt; {
    <span class="hljs-keyword">await</span> db.delete(<span class="hljs-string">'UserTopics'</span>).where({ userId });

    <span class="hljs-keyword">if</span> (topicSlugs.length === <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span>;

    <span class="hljs-keyword">const</span> allTopics = <span class="hljs-keyword">await</span> getTopics();
    <span class="hljs-keyword">const</span> slugToId = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>(allTopics.map(<span class="hljs-function"><span class="hljs-params">t</span> =&gt;</span> [t.slug, t.id]));

    <span class="hljs-keyword">const</span> topicsToInsert = topicSlugs.map(<span class="hljs-function"><span class="hljs-params">slug</span> =&gt;</span> {
      <span class="hljs-keyword">const</span> id = slugToId.get(slug);
      <span class="hljs-keyword">if</span> (!id) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Topic '<span class="hljs-subst">${slug}</span>' not found`</span>);
      <span class="hljs-keyword">return</span> { userId, topicId: id };
    });

    <span class="hljs-keyword">await</span> db.insert(<span class="hljs-string">'UserTopics'</span>).values(topicsToInsert);
  };

  <span class="hljs-keyword">return</span> { syncUserTopics };
};
</code></pre>
<p>That’s why we use Redis. Another would be, when anonymous users view public posts, you can’t afford to fetch the same content over and over. These users don’t carry state, but they bring volume. So we cache what’s static: metadata, categories and tags. We only fetch dynamic states like <code>isLiked</code> or <code>isFollowing</code> when needed.</p>
<p>Follow a simple rule: <strong>cache what doesn’t change often.</strong> This protects your database, improves response times, and keeps your system scalable.</p>
<h2 id="heading-final-thoughts">🎹 Final Thoughts</h2>
<p>I used to hate backend development 1 year and a half ago. But it was because I didn’t know those principles and I was constantly repeating myself, creating <strong>fancy</strong> implementations that didn’t scale and duplicating my code over and over again.</p>
<p>The good thing is that you don’t need to suffer through fragile environments, weird abstractions, or inconsistent APIs. You just need to build with intention. Whether it’s Docker, clean response adapters, DRY repositories, or Redis caching; every decision should move you closer to clarity, not complexity.</p>
<p>If you’re solo or in a small team, your codebase is your second brain. Don’t clutter it with patterns you don’t need (or understand) yet. Don’t delay delivery to sound smart. Build something that works, scales, and makes sense to the next person who reads it; even if that person is you, three months from now.</p>
<p>Honestly speaking, this is the guide I’d shared with my past self. I had to learn things the hard way. But you don’t have to. Remember. <strong>Never stop learning, don't be afraid of breaking things and always keep experimenting.</strong></p>
]]></content:encoded></item><item><title><![CDATA[10 Database Design Lessons Learned Working for a Startup]]></title><description><![CDATA[Hi! The abyss has been quite intense these weeks, but we're back with a new entry. I remember when I was working in the database design of my first position a year and a half ago. The amount of things I was blundering: bad practices, poor naming conv...]]></description><link>https://blog.itsfranciscoluna.com/10-database-design-lessons-learned-working-for-a-startup</link><guid isPermaLink="true">https://blog.itsfranciscoluna.com/10-database-design-lessons-learned-working-for-a-startup</guid><category><![CDATA[Databases]]></category><category><![CDATA[backend]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[SQL]]></category><dc:creator><![CDATA[Francisco Luna]]></dc:creator><pubDate>Sun, 31 Aug 2025 14:48:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1756651494870/0837e66e-d905-4ff4-ab8a-192892c15c3f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi! The abyss has been quite intense these weeks, but we're back with a new entry. I remember when I was working in the database design of my first position a year and a half ago. The amount of things I was blundering: bad practices, poor naming conventions... I guess that's how we all start and learn at the beginning.</p>
<p>I've been working on database design at a startup, and this time, it feels like the system is no longer against me. It's all about best practices, scalability, and clear conventions. But this didn't happen suddenly; it took countless mistakes, hours, and pain.</p>
<p>I've gone through the process of learning the best practices on my own and ensuring my schemas can actually scale, so you don't have to go through the same painful process. Here are the <strong>10 database design lessons</strong> I learned the hard way.</p>
<h2 id="heading-1-avoid-overusing-enums">1. Avoid overusing enums</h2>
<p>An <code>enum</code> (enumeration) is a data type that lets you define a list of possible string values a column can hold. The database then enforces that the data inserted into that column must be one of those values. They are great for small, unchanging sets of values but they're a pain to update because it requires a schema migration.</p>
<p><strong>Lookup tables</strong> are separate tables that contain a list of values, and your main table references them using a foreign key. This makes the values easy to manage, add, or remove without changing the main table's schema.</p>
<p><strong>Simple</strong> <code>Enum</code> in PostgreSQL:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TYPE</span> user_status_enum <span class="hljs-keyword">AS</span> ENUM (<span class="hljs-string">'active'</span>, <span class="hljs-string">'inactive'</span>, <span class="hljs-string">'suspended'</span>);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">users</span> (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    username <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">50</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    <span class="hljs-keyword">status</span> user_status_enum <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);
</code></pre>
<p><strong>Scalable Lookup Table Schema:</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> post_categories (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    <span class="hljs-keyword">name</span> <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">50</span>) <span class="hljs-keyword">UNIQUE</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> posts (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    title <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    category_id <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">REFERENCES</span> post_categories(<span class="hljs-keyword">id</span>)
);
</code></pre>
<h3 id="heading-backend-dictionaries">Backend Dictionaries</h3>
<p>These are data structures in your application code that hold a list of valid values for a specific field.</p>
<pre><code class="lang-ts"><span class="hljs-comment">// This data structure is dynamically populated from the database</span>
<span class="hljs-keyword">interface</span> CategoryDictionary {
 [categoryName: <span class="hljs-built_in">string</span>]: <span class="hljs-built_in">number</span>;
}

<span class="hljs-keyword">const</span> CATEGORIES: CategoryDictionary = {
 <span class="hljs-string">'Tech'</span>: <span class="hljs-number">1</span>,
 <span class="hljs-string">'Music'</span>: <span class="hljs-number">2</span>,
 <span class="hljs-string">'Travel'</span>: <span class="hljs-number">3</span>
};

<span class="hljs-comment">// Example usage</span>
<span class="hljs-keyword">const</span> categoryId = CATEGORIES[<span class="hljs-string">'Tech'</span>]; <span class="hljs-comment">// Returns 1</span>
<span class="hljs-keyword">const</span> isValidCategory = !!CATEGORIES[categoryName]; <span class="hljs-comment">// Checks if a category exists</span>
</code></pre>
<p>While they can be a useful tool for validation, simply using a static array of strings in the database isn't a robust solution on its own. It directly <strong>violates the principles of database normalization</strong> by creating a redundancy between your application and your database schema.</p>
<p>If the list of valid values changes, you have to update your code <strong>and</strong> potentially your database, leading to a disconnect between the two sources of truth. A more normalized approach is to use these dictionaries alongside a <strong>lookup table</strong>. The dictionary in your backend can be populated from the lookup table in your database on application startup. This way, the database remains the single source of truth for the list of values, and your application always has the most up-to-date data for validation.</p>
<h2 id="heading-2-design-for-the-business-not-for-convenience">2. Design for the Business, Not for Convenience</h2>
<p>This principle is about creating a database schema that accurately models the real-world business concepts, rather than what's easiest for your code. A <code>DATE</code> data type, for instance, represents a full calendar date. While a developer might find it convenient to store a birthday as three separate integers for year, month, and day, the business understands a birthday as a single date. Using the correct data type ensures your database reflects the truth of the business.</p>
<p><strong>Bad Design:</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> user_birthdays (
    user_id <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    birth_month <span class="hljs-built_in">INTEGER</span>,
    birth_day <span class="hljs-built_in">INTEGER</span>,
    birth_year <span class="hljs-built_in">INTEGER</span>
);
</code></pre>
<p><strong>Good Design:</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">users</span> (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    <span class="hljs-keyword">name</span> <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">100</span>),
    birthday <span class="hljs-built_in">DATE</span>
);
</code></pre>
<h2 id="heading-3-consistency-matters">3. Consistency Matters</h2>
<p><strong>Consistency</strong> means using the same naming conventions, data types, and structural patterns throughout your entire database. For example, if you choose <code>snake_case</code> (e.g., <code>user_id</code>), you should use it everywhere. Inconsistent naming like <code>user_id</code> in one table and <code>userId</code> in another leads to confusion and bugs.</p>
<p><strong>Inconsistent Naming (Bad):</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">users</span> (
    user_id <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>
);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> user_posts (
    post_id <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    userId <span class="hljs-built_in">INTEGER</span>
);
</code></pre>
<p><strong>Consistent Naming (Good):</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">users</span> (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>
);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> user_posts (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    user_id <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">REFERENCES</span> <span class="hljs-keyword">users</span>(<span class="hljs-keyword">id</span>)
);
</code></pre>
<h2 id="heading-4-everything-should-serve-a-purpose">4. Everything Should Serve a Purpose</h2>
<p>Every table, column, and index in your database should exist for a specific, current business requirement. <strong>Over-engineering</strong> is the act of building features or adding complexity that isn't currently needed, often with the thought of "just in case." This adds unnecessary complexity and can harm performance. The goal is to keep the database as simple as possible while meeting all requirements.</p>
<p><strong>Over-engineered (Bad):</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> products (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    shipping_notes <span class="hljs-built_in">TEXT</span>,
    supplier_id <span class="hljs-built_in">INTEGER</span>,
    is_on_sale <span class="hljs-built_in">BOOLEAN</span>,
    sale_price <span class="hljs-built_in">DECIMAL</span>(<span class="hljs-number">10</span>, <span class="hljs-number">2</span>),
);
</code></pre>
<p>If your business only has one supplier and products are never on sale, these columns are just clutter. <strong>Don't try to cover all possible future cases when a feature hasn't even been requested yet.</strong></p>
<h2 id="heading-5-dont-let-the-database-handle-authentication">5. Don't Let the Database Handle Authentication</h2>
<p>Your database is a data store, not a security server. Don't rely on features like PostgreSQL's <strong>Row-Level Security (RLS)</strong> for core authentication, please. It sounds easy to do, but you’re introducing provider-lock in. What if you change the database you’re working with in the future? That’s why all authentication and password hashing should be handled in your application layer.</p>
<p><strong>Example RLS Policy (to show what to avoid):</strong></p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Now your app's security is coupled to the DB...</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">POLICY</span> user_access <span class="hljs-keyword">ON</span> user_posts
    <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">SELECT</span>
    <span class="hljs-keyword">USING</span> (user_id = current_setting(<span class="hljs-string">'app.user_id'</span>)::<span class="hljs-built_in">integer</span>);
</code></pre>
<h2 id="heading-6-the-database-shouldnt-handle-business-logic-validations">6. The Database Shouldn't Handle Business Logic Validations</h2>
<p><strong>Validation</strong> is the process of ensuring data is correct and meaningful. The database should only perform basic validations like ensuring a field is not null or that a value is unique. Complex business logic, like checking if a number is between 1 and 12 for a month, should be handled by your application code. This separation of concerns keeps your database clean and your application flexible.</p>
<p><strong>Application-level validation (Better):</strong></p>
<pre><code class="lang-ts"><span class="hljs-comment">// In your backend code</span>
<span class="hljs-keyword">if</span> (month &lt; <span class="hljs-number">1</span> || month &gt; <span class="hljs-number">12</span>) {
  <span class="hljs-comment">// Error handling</span>
}
</code></pre>
<h2 id="heading-7-separate-staging-and-production-databases">7. Separate Staging and Production Databases</h2>
<p><strong>Production</strong> is the public-facing environment. <strong>Staging</strong> is a pre-production environment used to test new features. You should never work directly on your production database. Mistakes are inevitable. Having separate databases creates a safety net, so you don't accidentally delete real customer data. I recommend you using a product like <strong>Hashicorp Vault</strong> to properly save the credentials and URLs to work with these environments.</p>
<h2 id="heading-8-never-take-scalability-for-granted">8. Never Take Scalability for Granted</h2>
<p><strong>Scalability</strong> is the ability of a system to handle a growing amount of work. It’s a mindset you must adopt from the beginning. A key part of this is understanding the impact of your design choices. An <strong>index</strong> is a data structure that improves the speed of data retrieval operations on a database table. Forgetting to add them to foreign keys is a common mistake that can lead to major performance issues as your data grows.</p>
<h3 id="heading-adding-an-index">Adding an Index</h3>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> user_posts (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    user_id <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">REFERENCES</span> <span class="hljs-keyword">users</span>(<span class="hljs-keyword">id</span>)
);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> idx_user_posts_user_id <span class="hljs-keyword">ON</span> user_posts (user_id);
</code></pre>
<h3 id="heading-think-about-the-long-term">Think About the Long Term</h3>
<p>Always build your tables with the expectation that there will be changes and new features.</p>
<p><strong>Spoiler:</strong> There will always be changes.</p>
<p>This means you should design for <strong>the long term</strong>. A critical part of this is adding a new column. To avoid database conflicts during a migration, a new column <strong>must be nullable or have a default value</strong>. This prevents live applications from failing when they try to insert new data. While a nullable column is a common and necessary workaround, using a default value is often a better practice from a data integrity standpoint, as it avoids nulls and maintains a cleaner dataset.</p>
<h3 id="heading-document-your-schemas">Document Your Schemas</h3>
<p>In a startup, your data structures will evolve quickly. When using flexible data types like <code>JSON</code> or <code>JSONB</code> to store semi-structured data, it's easy to lose track of the schema. That's why you should <strong>document your JSON/JSONB schemas and structures through a formal specification like OpenAPI</strong>. This ensures that your entire team and any future consumers of your API understand the data format, preventing errors and ensuring consistency.</p>
<h2 id="heading-9-use-the-right-data-structures">9. Use the Right Data Structures</h2>
<p>Choosing the correct data type (e.g., <code>BOOLEAN</code>, <code>DATE</code>, <code>JSONB</code>) for a column is important for performance and clarity. <code>JSONB</code> is a PostgreSQL data type that stores JSON data in a binary format which allows you to index and query it efficiently. It’s perfect for semi-structured data where the schema might be flexible.</p>
<p><strong>Example with</strong> <code>JSONB</code>:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> social_media_posts (
    <span class="hljs-keyword">id</span> <span class="hljs-keyword">UUID</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    metadata JSONB
);

<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> social_media_posts (<span class="hljs-keyword">id</span>, metadata)
<span class="hljs-keyword">VALUES</span> (
    <span class="hljs-string">'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'</span>,
    <span class="hljs-string">'{"tags": ["newmusic"], "likes": 150}'</span>
);
</code></pre>
<h2 id="heading-10-always-choose-uuids-over-autoincrement-ids">10. Always Choose UUIDs Over <code>auto_increment</code> IDs</h2>
<p>An <code>auto_increment</code> ID is a simple integer that automatically increases with each new row. A <strong>UUID (Universally Unique Identifier)</strong> is a 128-bit number that is globally unique. UUIDs are essential for distributed systems because they can be generated anywhere, on a client, in a microservice; without a central authority to ensure uniqueness. This prevents ID conflicts and makes your system much more scalable and secure.</p>
<p><strong>Why UUIDs are better:</strong></p>
<ul>
<li><p><strong>No Conflicts in Distributed Systems:</strong> With <code>auto_increment</code>, two different services inserting data at the same time might generate the same ID. UUIDs solve this problem.</p>
</li>
<li><p><strong>Security &amp; Data Leaks:</strong> Predictable, sequential IDs like <code>1, 2, 3...</code> are a major security risk. A malicious user can simply try incrementing IDs in an API endpoint (e.g., <code>/api/users/1</code>, then <code>/api/users/2</code>) to scrape or access other users' data. With UUIDs, the IDs are random and impossible to guess, protecting against these types of attacks.</p>
</li>
</ul>
<p><strong>Example in PostgreSQL:</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">users</span> (
    <span class="hljs-keyword">id</span> <span class="hljs-keyword">UUID</span> PRIMARY <span class="hljs-keyword">KEY</span> <span class="hljs-keyword">DEFAULT</span> gen_random_uuid()
    email <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">100</span>) <span class="hljs-keyword">UNIQUE</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);

<span class="hljs-comment">-- The UUID is generated automatically</span>
<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> <span class="hljs-keyword">users</span> (email) <span class="hljs-keyword">VALUES</span> (<span class="hljs-string">'janedoe@example.com'</span>);
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Database design is a brutal teacher in software engineering. The most painful lessons aren't in a tutorial when you're developing; they're in production, where a small mistake can become a colossal monster. Don't be discouraged by your blunders or failures as they're the very thing that forges your mastery.</p>
<p>Think of it like Dark Souls. You don’t get good by avoiding the bosses; you get good by facing them, learning their patterns, and finally beating them. The ten lessons in this post are your strategies. Now you're ready to face your own production monsters. I know I'll be facing more challenges, and I'm willing to keep sharing with you everything I learn on this journey.</p>
<p>See you in the next entry. Don’t let the abyss consume you.</p>
]]></content:encoded></item><item><title><![CDATA[What 41 Users in 4 Days Taught Me with my First Browser Extension]]></title><description><![CDATA[I've launched Veelo, a browser extension which you can use to take notes within each tab, kind of like a minimalist Notion in your browser this Wednesday 13th in August.
By the day I'm writing this, in August 17th we've gotten:

41 people across 4 co...]]></description><link>https://blog.itsfranciscoluna.com/what-41-users-in-4-days-taught-me-with-my-first-browser-extension</link><guid isPermaLink="true">https://blog.itsfranciscoluna.com/what-41-users-in-4-days-taught-me-with-my-first-browser-extension</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[startup]]></category><category><![CDATA[Startups]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[learning]]></category><dc:creator><![CDATA[Francisco Luna]]></dc:creator><pubDate>Sun, 17 Aug 2025 20:55:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755461358555/8576ef1c-50b7-4257-ac4a-2f1d622ad371.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've launched <a target="_blank" href="https://chromewebstore.google.com/detail/mpncifafcdafiholmcmbdefjnkcmajef?utm_source=item-share-cb">Veelo,</a> a browser extension which you can use to take notes within each tab, kind of like a minimalist Notion in your browser this Wednesday 13th in August.</p>
<p>By the day I'm writing this, in August 17th we've gotten:</p>
<ul>
<li><p>41 people across 4 continents using Veelo</p>
</li>
<li><p>1 rating</p>
</li>
</ul>
<p>That’s 10 people per continent! It's all been organic traffic, networking and reaching out to existing communities. I'm really proud of it so far.</p>
<p>The process sounds fun and it seems like I'm winning. Maybe I’m, but I've only ensured to follow a few key principles after <strong>several</strong> failed products and ideas of mine, hence why I believe things are actually working out this time.</p>
<p>It's not luck. It's engineering, psychology and sales. Similar to Dark Souls, I had to "die" <strong>a lot</strong> to get good at this game.</p>
<p>Let me share my lessons with you and what's working this time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755461737266/a6f02df0-1602-4d3d-95e9-3c0884016cf5.webp" alt class="image--center mx-auto" /></p>
<h2 id="heading-you-dont-need-to-reinvent-the-wheel">You don't need to reinvent the wheel</h2>
<p>Let's start by clarifying something. Veelo <strong>is not</strong> a tool to highlight notes in a website primarily. It's used as a smaller version of Note which you can carry across different websites. Each website you visit can have their own notes, and you can also pin notes, making them show up in almost all websites you visit.</p>
<p>I'd also tested <a target="_blank" href="https://chromewebstore.google.com/detail/note-anywhere/bohahkiiknkelflnjjlipnaeapefmjbh">Note Anywhere</a>, another good extension to work with sticky notes. But I found a few problems with the UX/UI. You cannot use markdown, you cannot highlight text, you cannot name notes and the UI feels intrusive sometimes.</p>
<p>It served as an inspiration to build Veelo, which combines the UX/UI of Notion with the capabilities of Note Anywhere, and beyond!</p>
<p><strong>The lesson? Remix and take inspiration from others. 🚀</strong></p>
<h2 id="heading-build-with-and-for-actual-users">Build with and for actual users</h2>
<p>One of the reasons why Veelo has been well received by the first users is because of how easy it's to use. But it wasn't always like this! While working with students in the development stage, I noticed important problems and limitations:</p>
<ul>
<li><p>Everything was markdown based (A language used primarily by technical users)</p>
</li>
<li><p>No menu or guidance for the average user</p>
</li>
<li><p>No highlighters</p>
</li>
<li><p>Notes didn't even have titles</p>
</li>
</ul>
<p><em>I regret making Veelo this technical at the beginning to be honest. Beta testers literally saved the product, I couldn’t be more grateful for that.</em></p>
<p>While it was brilliant, I was clearly missing basic functionalities to make it useful.</p>
<p>That’s when I learned the following: never build alone. Always ask for feedback and recommendations, even if it comes from friends. Otherwise you're just building for a void. And you actually want people to use your products.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755462669113/5d65dbcf-19a2-4d0a-9438-b230d6931db7.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-dont-limit-your-target-audience">Don't limit your target audience</h2>
<p>Veelo initially was built only for developers, as I was trying to solve my own problem of multitasking between technical documentation from work, and my browser. This didn’t work out because:</p>
<ul>
<li><p>My problem was almost non-existent given all tools available for developers</p>
</li>
<li><p>Students struggled the most with similar problems</p>
</li>
<li><p>There were better alternatives for developers. Yet the average user or student didn’t have much intuitive tools</p>
</li>
</ul>
<p>Hence why I ended up pivoting the idea from a developers tool to one for students and non-technical users. Devs can also benefit from it, as well!</p>
<h2 id="heading-launch-is-just-the-beginning">Launch is just the beginning</h2>
<p>Sure, you launched your product and gathered your first users. But if you want to keep helping people with it and accomplish your goals, you actually need to have a long term plan.</p>
<p>It could be:</p>
<ol>
<li><p>Reaching 762 users by December and 52 daily active users</p>
</li>
<li><p>Featuring in Dev.to or HackerNews</p>
</li>
<li><p>Monetizing one feature by January 2026</p>
</li>
</ol>
<p>But Fran, why are you this strict? My friend, if you're <strong>really</strong> serious about your growth:</p>
<ul>
<li><p>It forces you to find a way to accomplish those goals</p>
</li>
<li><p>It helps you think about the long term. Launching product is fun. But the process of networking, scaling and following long term strategies will required you to be disciplined and have a plan.</p>
</li>
</ul>
<p><strong>You might get far without a plan. But even a bad plan is better than no plan at all.</strong></p>
<h2 id="heading-next-plans">Next Plans</h2>
<p>Next time, I’ll reveal the hardest engineering decisions I had to make (and how they almost killed the product) in a study case. I’ll keep focused on learning from user feedback, improving my skills at work and keep sharing my knowledge.</p>
<p>Thank you so much for reading! You can follow my journey on <a target="_blank" href="https://www.linkedin.com/in/franciscoluna28/">LinkedIn</a> as well. Let’s keep fighting the abyss together.</p>
]]></content:encoded></item><item><title><![CDATA[How I'm Escaping the Startup Grind Before Burning Out (At 21 With 2 YOE)]]></title><description><![CDATA[Hi, I’m Francisco Luna, Full - Stack Engineer. Let me tell you how I almost lost myself because of the startup grind and what you can learn from it.
The sword Of Damocles
July 2025. I had just finished university. Stable job, hobbies, freedom, a soci...]]></description><link>https://blog.itsfranciscoluna.com/how-im-escaping-the-startup-grind-before-burning-out-at-21-with-2-yoe</link><guid isPermaLink="true">https://blog.itsfranciscoluna.com/how-im-escaping-the-startup-grind-before-burning-out-at-21-with-2-yoe</guid><category><![CDATA[Self Improvement ]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[startup]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Francisco Luna]]></dc:creator><pubDate>Fri, 15 Aug 2025 19:31:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/cILIC2Ws6_s/upload/f5536090b5c5c8c7004890545173cc10.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi, I’m Francisco Luna, Full - Stack Engineer. Let me tell you how I almost lost myself because of the startup grind and what you can learn from it.</p>
<h3 id="heading-the-sword-of-damocles">The sword Of Damocles</h3>
<p>July 2025. I had just finished university. Stable job, hobbies, freedom, a social life. It was all “good”. Yet I felt like a bird in a cage; exhausted, uninspired and trapped in routines that weren’t even mine.</p>
<p>I started to lose my passion for building outside of work. Tasks and tickets mixed together. I was in an emotional limbo. My family asked if I was okay. I wasn’t. I was documenting every lesson learned at work: From public speaking, leading complex refactors to handling life changes; all at once.</p>
<p>Then I came across the book <em>Buy Back Your Time</em> by Dan Martell. One quote hit me like a lightning bolt:</p>
<blockquote>
<p>“If your business depends on you, you don’t own a business—you have a job. And it’s the worst job in the world because you’re working for a lunatic!”</p>
</blockquote>
<p>All of a sudden it clicked. I was undervalued. My skills, my creativity, my ownership; they weren’t mine. They were borrowed. And when the startup stalled, so did I.</p>
<hr />
<h3 id="heading-the-startup-illusion-in-2025">The Startup Illusion in 2025</h3>
<p><strong>The first 5 months (Heaven):</strong></p>
<ul>
<li><p>Built our payment system from scratch</p>
</li>
<li><p>Scaled real-time features to 1K+ concurrent users</p>
</li>
<li><p>Designed our Redis caching layer</p>
</li>
</ul>
<p>This was pure flow state and why I fell in love with coding.</p>
<p><strong>Then reality hit:</strong></p>
<ul>
<li><p>Renaming variables just to “do something”</p>
</li>
<li><p>Sitting through two hour compliance meetings</p>
</li>
<li><p>“Refactoring” perfectly working code</p>
</li>
</ul>
<p>What kind of engineering was this? My passion was just dying.</p>
<hr />
<h3 id="heading-the-wake-up-call">The Wake-Up Call</h3>
<p>That week, I realized:</p>
<ul>
<li><p>Skills stagnating</p>
</li>
<li><p>Creativity dying</p>
</li>
<li><p>Passion fading</p>
</li>
<li><p>Worst of all, I’d stopped building for myself</p>
</li>
</ul>
<p>If I only built at work, my growth was limited by the company’s pace, bureaucracy, and whatever stage the startup was in. I had to reclaim my ownership.</p>
<hr />
<h3 id="heading-the-escape-plan">The Escape Plan</h3>
<p>To start claiming back my freedom I launched <a target="_blank" href="https://chromewebstore.google.com/detail/mpncifafcdafiholmcmbdefjnkcmajef?utm_source=item-share-cb"><strong>Veelo</strong></a>, a browser extension that:<br />✓ Captures ideas instantly<br />✓ Organizes technical notes<br />✓ Preserves brilliant “shower thoughts”</p>
<p>It started as a prototype in May 2025 when I opened a white IDE and I had a crazy idea. It gathered 21 users the first day, which doesn’t sound like a lot. But it meant my work was helping other people somehow.</p>
<p>The best part? No stakeholders. No pointless meetings. Just me solving real problems for myself. Coding was fun again. And I remembered why I started building in the first place: curiosity, ownership and mastery.</p>
<p>That’s when I felt alive again after months</p>
<hr />
<h3 id="heading-next-plans">Next Plans</h3>
<p>Veelo was just the beginning. I want to keep building meaningful projects, solve problems I care about, and push my skills beyond the 9 to 5.</p>
<p>I’ll share the wins, the failures and the messy middle. <strong>Because potential isn’t something your employer gives you, it’s something you own.</strong></p>
<p>Work will stall and bureaucracy will slow you down. But the things you build for yourself? They’re yours. They grow you. They teach you, and set you free.</p>
<p>Thanks for reading. I only want you to keep this simple reminder in mind from now on:</p>
<p><em>Don’t ever go hollow building someone else’s empire only.</em></p>
]]></content:encoded></item></channel></rss>