<?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>Thu, 16 Apr 2026 11:14:07 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[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>