<?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[Dileepa Ranawake]]></title><description><![CDATA[Insights and lessons from a full-stack engineer building in public, shaped by interests in AI, accessibility, education and tech for good.]]></description><link>https://dileeparanawake.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1750870219570/a4a29de3-f97e-464e-8a42-e9a271412a03.png</url><title>Dileepa Ranawake</title><link>https://dileeparanawake.com</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 18 Apr 2026 10:01:23 GMT</lastBuildDate><atom:link href="https://dileeparanawake.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Surface-First Schema Design]]></title><description><![CDATA[When I began building LittleSteps AI - a full-stack, authenticated LLM chat for new parents - I assumed the database schema should come first.
I was wrong.
The behaviour of the system’s surfaces - the UI, auth layer, and OpenAI provider - revealed th...]]></description><link>https://dileeparanawake.com/surface-first-schema-design</link><guid isPermaLink="true">https://dileeparanawake.com/surface-first-schema-design</guid><category><![CDATA[schema]]></category><category><![CDATA[full stack]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[llm]]></category><category><![CDATA[modelling]]></category><category><![CDATA[Databases]]></category><category><![CDATA[architecture]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Case Study]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Dileepa Ranawake]]></dc:creator><pubDate>Thu, 20 Nov 2025 17:32:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763659269427/aca48dac-0749-4533-88d3-6b6dfa79a27c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I began building <a target="_blank" href="https://github.com/dileeparanawake/littlesteps-ai">LittleSteps AI</a> - a full-stack, authenticated LLM chat for new parents - I assumed the database schema should come first.</p>
<p>I was wrong.</p>
<p>The behaviour of the system’s surfaces - the UI, auth layer, and OpenAI provider - revealed the real rules the schema needed to uphold long before any abstract modelling exercise could.</p>
<p>Surfaces are the concrete touchpoints you can actually see and test. They expose real behaviour and constraints, which makes them a far more reliable foundation for modelling than guessing at tables.</p>
<p>This post is a case study in how a surface-first approach helped me design a smaller, clearer, more reliable schema that matched the product instead of my assumptions.</p>
<h2 id="heading-1-the-problem-i-needed-to-solve"><strong>1. The Problem I Needed to Solve</strong></h2>
<p>LittleSteps AI had to support:</p>
<ul>
<li><p>authenticated Google users</p>
</li>
<li><p>persistent chat threads</p>
</li>
<li><p>ordered messages</p>
</li>
<li><p>thread/URL navigation</p>
</li>
<li><p>OpenAI-powered prompts and responses</p>
</li>
</ul>
<p><strong>Core problem:</strong> The real difficulty wasn’t building a chat UI - it was deciding what a minimal schema looked like, and whether the backend or frontend should drive the design.</p>
<p>As I was thinking about schemas (and feeling a little lost), two things became clearer when I mapped user flows and built a quick minimal frontend:</p>
<ul>
<li><p><strong>(1) The user experience had a fixed shape</strong>: A predictable flow: sidebar → thread list → thread rename → message list → new message appended.</p>
</li>
<li><p><strong>(2) The behaviour of the UI wasn’t optional</strong>: It imposed concrete rules the backend had to respect: Those behavioural constraints turned out to be far more informative than starting with tables and guessing relationships.</p>
</li>
<li><p><strong>(3) The model provider exposed strict sequencing and message structure</strong>:</p>
<p>  The OpenAI request–response cycle surfaced rules around ordering, role classification, and token metadata - constraints that the schema would eventually need to include.</p>
</li>
<li><p><strong>(4) The auth layer brought its own structural boundaries</strong>:</p>
<p>  BetterAuth came with a working user/session/account schema and strict ownership hierarchies. Instead of reinventing identity, these boundaries became another “surface” that shaped the final data model.</p>
</li>
</ul>
<p>By mapping early UI flows and observing the behaviour of the auth layer and model provider, the actual ‘surfaces’ of the system became clear.</p>
<h2 id="heading-2-constraints-what-made-this-non-trivial"><strong>2. Constraints: What Made This Non-Trivial</strong></h2>
<p>The behaviour of the UI, the auth layer, and the OpenAI provider surfaced constraints that the schema had to include:</p>
<ul>
<li><p><strong>Deterministic ordering</strong><br />  Messages must appear exactly in the order the user expects - no races, no timestamp drift.</p>
</li>
<li><p><strong>Non-guessable identifiers</strong><br />  Thread IDs appear in the browser URL and must be safe to share.</p>
</li>
<li><p><strong>Strict ownership</strong><br />  Only the creator of a thread should ever access it.</p>
</li>
<li><p><strong>No orphaned entities</strong><br />  A thread delete should cascade messages; a message must never “float”.</p>
</li>
<li><p><strong>GDPR-aligned deletion</strong><br />  Removing a user should remove data across auth sessions, accounts, threads, and messages.</p>
</li>
<li><p><strong>Room for growth without premature complexity</strong><br />  Token usage, analytics, or rate limits might come later-but shouldn’t distort the minimal model now.</p>
</li>
<li><p><strong>Atomic prompt–response cycles</strong><br />  Each user prompt produces a tightly coupled pair of messages (user → assistant), which must be persisted together in the correct order.</p>
</li>
</ul>
<p>Together, these constraints narrowed the viable designs. Once the rules were visible, only a small, clean schema actually fit them.</p>
<h2 id="heading-3-why-surface-first-not-schema-first-led-to-a-better-model"><strong>3. Why Surface-First (Not Schema-First) Led to a Better Model</strong></h2>
<p>My core insight wasn’t “UI-first”.</p>
<p>It was <strong>surface-first</strong>: using the concrete surfaces of the system (UI, auth layer, and model provider etc) to reveal the concrete behaviours and rules the schema must design for.</p>
<p>To do that, my mental model is:</p>
<h3 id="heading-surface-rules-schema"><strong>Surface → Rules → Schema</strong></h3>
<p><em>(Surface (behaviours) → Rules (invariants) → Schema)</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763659116064/e291296f-6317-4dd6-be6b-4e6f5dc61c42.png" alt="Diagram showing surface first mental model" class="image--center mx-auto" /></p>
<p>This model works because the behaviour of a system’s surfaces always constrains what the schema must guarantee.</p>
<h4 id="heading-the-behavioural-inputs-came-from-three-places">The behavioural inputs came from three places:</h4>
<p><strong>a) The UI</strong></p>
<ul>
<li><p>What does the sidebar need to load?</p>
</li>
<li><p>How must threads appear in order?</p>
</li>
<li><p>How do we navigate after creating a new message?</p>
</li>
<li><p>Where does rename write to?</p>
</li>
</ul>
<p><strong>b) The OpenAI model provider</strong><br />Its request-response cycle surfaces constraints around:</p>
<ul>
<li><p>atomic prompt+response writes</p>
</li>
<li><p>message role classification</p>
</li>
<li><p>token metadata</p>
</li>
<li><p>error handling paths</p>
</li>
</ul>
<p><strong>c) The auth layer (BetterAuth)</strong><br />It defines ownership, session guarantees, and the edges of who can access what.</p>
<h4 id="heading-what-this-revealed">What this revealed</h4>
<p>Answering those questions produced a set of invariants:</p>
<ul>
<li><p>Threads belong to exactly one user</p>
</li>
<li><p>Messages belong to exactly one thread</p>
</li>
<li><p>Messages require stable ordering</p>
</li>
<li><p>Thread titles must be constrained</p>
</li>
<li><p>URLs must use non-enumerable IDs</p>
</li>
<li><p>Delete-user must delete everything cleanly</p>
</li>
</ul>
<p>Once the invariants were clear, the schema became straightforward to design.</p>
<h4 id="heading-why-i-didnt-start-with-schema-first">Why I didn’t start with schema-first</h4>
<p>Schema-first would have forced:</p>
<ul>
<li><p>guessing relationships before understanding flows</p>
</li>
<li><p>modelling features that didn’t exist</p>
</li>
<li><p>encoding constraints that didn’t reflect behaviour</p>
</li>
<li><p>hiding edge cases the UI and provider would later surface</p>
</li>
</ul>
<p>Schema-first is <strong>low resolution</strong>-it shows what entities might exist, but it doesn’t show <em>how</em> they behave.</p>
<p>Surface-first revealed the dynamics that the schema needed to preserve.</p>
<p>Each surface exposed a different part of the real behaviour: the UI revealed navigation and ordering, the model provider revealed atomic cycles, and the auth layer revealed ownership boundaries.</p>
<h2 id="heading-4-the-data-model"><strong>4. The Data Model</strong></h2>
<p>Combining UI flow, auth guarantees, and OpenAI’s request–response lifecycle, the schema settled into four tables:</p>
<p>Each part of the schema maps directly back to the constraints surfaced earlier. The UI revealed ordering and navigation behaviour, which drove the need for explicit sequences and non‑enumerable IDs. The model provider revealed atomic prompt–response cycles and role structures, which shaped the message table. The auth layer exposed strict ownership boundaries, which defined the thread → user relationship and cascade rules. By tying each schema choice to a rule revealed by a surface, the final model became smaller, more explicit, and directly aligned with real product behaviour.</p>
<p><strong>User</strong>, <strong>Session/Account</strong>, <strong>Thread</strong>, <strong>Message</strong>.</p>
<pre><code class="lang-txt">User (id TEXT PK, email UNIQUE, name, createdAt, updatedAt)
 ├─ Session (id TEXT PK, token UNIQUE, expiresAt, userId FK → User.id ON DELETE CASCADE)
 ├─ Account (id TEXT PK, accountId, providerId, userId FK → User.id ON DELETE CASCADE)
 └─ Thread (id UUID PK, userId FK → User.id ON DELETE CASCADE, title VARCHAR(60), createdAt, updatedAt)
     └─ Message (id UUID PK, threadId FK → Thread.id ON DELETE CASCADE,
                 sequence INT, role ENUM[system|user|assistant], content TEXT,
                 createdAt, promptTokens?, completionTokens?, totalTokens?,
                 UNIQUE(threadId, sequence))
</code></pre>
<h3 id="heading-what-this-structure-guarantees"><strong>What This Structure Guarantees</strong></h3>
<h4 id="heading-1-clear-ownership"><strong>1. Clear ownership</strong></h4>
<p>A single <code>userId</code> foreign key on <code>Thread</code> encodes all access rules.<br />The API only needs one join to enforce permissions. This directly satisfies the ownership boundaries revealed by the auth surface.</p>
<h4 id="heading-2-no-orphaned-data"><strong>2. No orphaned data</strong></h4>
<p><code>ON DELETE CASCADE</code> removes dependent rows automatically.<br />This makes GDPR “delete my data” flows trivial. This addresses the data‑lifecycle rules exposed by both the UI surface and GDPR deletion requirements.</p>
<h4 id="heading-3-deterministic-message-ordering"><strong>3. Deterministic message ordering</strong></h4>
<p>Timestamps are unreliable under async workloads and parallel writes.<br />Instead, the schema uses:</p>
<ul>
<li><p>a single source of truth: <code>sequence</code></p>
</li>
<li><p>DB-level protection: <code>UNIQUE(threadId, sequence)</code></p>
</li>
</ul>
<p>This guarantees consistent ordering for:</p>
<ul>
<li><p>UI rendering</p>
</li>
<li><p>API consumers</p>
</li>
<li><p>DB-backed tests (Vitest)</p>
</li>
<li><p>debugging race conditions</p>
</li>
</ul>
<p>Deterministic ordering emerged as a key requirement from several surfaces. I hadn’t anticipated it early on, but it became clear very quickly that relying on timestamps would produce inconsistencies. This solves the ordering and atomicity constraints exposed by both the UI and the model‑provider surfaces.</p>
<h4 id="heading-4-safe-urls"><strong>4. Safe URLs</strong></h4>
<p>UUIDs make thread/message identifiers:</p>
<ul>
<li><p>non-enumerable</p>
</li>
<li><p>shareable</p>
</li>
<li><p>URL-safe</p>
</li>
<li><p>predictable in routing</p>
</li>
</ul>
<p>No need for obfuscation or extra indirection. This satisfies the routing and non‑enumerability requirements imposed by the UI surface.</p>
<h4 id="heading-5-practical-constraints-exposed-by-the-ui-surface"><strong>5. Practical constraints exposed by the UI surface</strong></h4>
<p>The UI forced the schema to adopt:</p>
<ul>
<li><p>title length limits</p>
</li>
<li><p>stable sequence ordering</p>
</li>
<li><p>UUID routing</p>
</li>
<li><p>role validation (<code>system | user | assistant</code>)</p>
</li>
</ul>
<p>Every constraint is tied to real behaviour-not speculation. These schema constraints originate directly from the behavioural limits surfaced in the UI.</p>
<h2 id="heading-5-trade-offs-i-made-to-ship-faster"><strong>5. Trade-offs I Made to Ship Faster</strong></h2>
<ul>
<li><p><strong>1. Some rules (invariants) live in app logic, not the DB</strong><br />  Ownership checks and sequence assignment happen in the application layer.<br />  This keeps the schema simple but shifts responsibility to well-tested code.</p>
</li>
<li><p><strong>2. Single-user threads only</strong><br />  Supporting shared threads would require membership tables and more complex ACLs.<br />  Out of scope for MVP - so the model stayed intentionally simple.</p>
</li>
<li><p><strong>3. No configurable system prompts yet</strong><br />  I excluded per-thread system messages; future UX changes will require schema migrations.<br />  This avoided premature abstraction.</p>
</li>
<li><p><strong>4. Shared Postgres instance during tests</strong><br />  Vitest runs tests concurrently, so I used one Postgres container for all tests.<br />  DB-backed tests validated:</p>
<ul>
<li><p>thread creation</p>
</li>
<li><p>message append</p>
</li>
<li><p>ordering guarantees</p>
</li>
<li><p>deletion behaviour</p>
</li>
</ul>
</li>
</ul>
<p>    Per-test isolated DBs would reduce async testing bugs and improve test isolation - but at the cost of slower iteration. I chose a shared database first to keep feedback loops fast.</p>
<ul>
<li><strong>5. MVP focus over performance/security</strong><br />  I explicitly avoided deep optimisation or hardening (RLS, rate-limits, encryption, extra indexes) until the product stabilises.</li>
</ul>
<h2 id="heading-6-closing-thoughts-surface-first-schema-design"><strong>6. Closing Thoughts: Surface-First Schema Design</strong></h2>
<p>The biggest lesson from LittleSteps was clear:</p>
<blockquote>
<p><strong>Schema design becomes clearer when you start from real behaviour, not tables.</strong></p>
</blockquote>
<p>By grounding everything in UI flows, auth rules, and provider interactions, the backend became:</p>
<ul>
<li><p>smaller and more explicit</p>
</li>
<li><p>easier to reason about</p>
</li>
<li><p>easier to test</p>
</li>
<li><p>aligned with real product constraints</p>
</li>
<li><p>capable of evolving without rewrites</p>
</li>
</ul>
<p>The process wasn’t “UI-first” so much as <strong>surface-first</strong> - starting from the external surfaces of the system to understand the rules it must uphold.</p>
<p>I appreciate this is more difficult in a production system with large volumes of existing data, but for early product stages it enables fast iteration and clearer modelling.</p>
<p>If you’re building a full-stack app - especially one for quick prototypes / fast itteration / with chat-like interactions / LLM workflows - try this pattern:</p>
<h4 id="heading-surface-rules-schema-1"><strong>Surface → Rules → Schema</strong></h4>
<p>Model the real flows first.<br />Let the schema fall out of them.<br />You may find that the architecture becomes simpler, more reliable, and easier to extend.</p>
]]></content:encoded></item><item><title><![CDATA[When Optimism Becomes Delusion]]></title><description><![CDATA[Optimism becomes delusion when we STOP absorbing feedback.
If we become deluded, we get blind spots and we can miss the things that will sink us. 
For me personally this is why one of the most important principles of the Agile manifesto is “At regula...]]></description><link>https://dileeparanawake.com/optimism-vs-delusion</link><guid isPermaLink="true">https://dileeparanawake.com/optimism-vs-delusion</guid><category><![CDATA[agile]]></category><category><![CDATA[software development]]></category><category><![CDATA[Retrospective]]></category><dc:creator><![CDATA[Dileepa Ranawake]]></dc:creator><pubDate>Tue, 22 Jul 2025 10:40:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753181305979/e8279cbd-cc03-4f68-a889-29afb0d21254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Optimism becomes delusion when we STOP absorbing feedback.</p>
<p>If we become deluded, we get blind spots and we can miss the things that will sink us. </p>
<p>For me personally this is why one of the most important principles of the Agile manifesto is “At regular intervals, the team reflects on how to become more effective, then tunes and adjusts its behaviour accordingly.”</p>
<p>Second to that it is: “the Art of maximising work NOT done" <a target="_blank" href="https://dileeparanawake.com/minimum-viable-slice">read more</a>. </p>
<p>Feedback, retrospectives, iterating the scope is key. This applies in my technical solo projects, my team projects and in life. </p>
<p>You’re not delusional when you’re wrong.</p>
<p>You’re delusional when you <strong>don’t test if you’re wrong</strong>.</p>
<p>Build a small, meaningful part — get feedback — adjust.</p>
<p>Agile has become such a buzzword and a certified set of mindless rigid processes I feel the intent of agile is often lost in the processes. </p>
<p>I think we need to get back to the intent, and less of the performance.</p>
<p>What’s your favourite Agile principle? Why?</p>
]]></content:encoded></item><item><title><![CDATA[Learning to Think Like an Engineer: The Day I Stopped Just Writing Code]]></title><description><![CDATA[TL;DR

“Coding is execution. Engineering is understanding.”I didn’t realise the difference — until I paused the build to map the architecture.

Why I Had to Stop Before I Even Started
I sat down to begin building the first slice of my new project — a...]]></description><link>https://dileeparanawake.com/learning-to-think-like-an-engineer</link><guid isPermaLink="true">https://dileeparanawake.com/learning-to-think-like-an-engineer</guid><category><![CDATA[full stack]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[architecture]]></category><category><![CDATA[learning]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[llm]]></category><category><![CDATA[Junior developer ]]></category><dc:creator><![CDATA[Dileepa Ranawake]]></dc:creator><pubDate>Fri, 11 Jul 2025 07:56:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752220176599/84cb3c28-c8f2-4171-96b2-f493df509ca8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>TL;DR</p>
<blockquote>
<p><strong>“Coding is execution. Engineering is understanding.”</strong><br /><em>I didn’t realise the difference — until I paused the build to map the architecture.</em></p>
</blockquote>
<h2 id="heading-why-i-had-to-stop-before-i-even-started">Why I Had to Stop Before I Even Started</h2>
<p>I sat down to begin building the first slice of my new project — a simple LLM app for parents — and... roadblock time 😬</p>
<p>It wasn’t a motivation issue. I had a good sense of what the app should do and a rough stack in mind: Next.js, Drizzle, OpenAI, Zod, OAuth. But something felt off.</p>
<p>The problem, I realised, wasn’t that I didn’t know how to code — it was that I didn’t know how it all fit together. I couldn’t see the shape of the system I was trying to build. And without that, I didn’t know how to start, or how to be confident in my stack and what I was building.</p>
<p>So I paused.</p>
<p>Before jumping into code, I decided I needed to understand the architecture — the flow, the boundaries, the responsibilities. I didn’t want to just <em>write</em> code. I wanted to understand how the system <em>worked</em>.</p>
<p>This post is a reflection on what I did, what I learned, and why this was one of the most important early steps I’ve taken in learning to think like a full-stack engineer.</p>
<h2 id="heading-why-i-paused-i-hadish-the-tech-stack-but-not-the-system">Why I Paused: I Had(ish) the Tech Stack — But Not the System</h2>
<p>I’d already chosen my stack. I had a rough idea of the libraries I planned to use. But I didn’t fully understand <em>how</em> they fit together — or what each one was responsible for.</p>
<p>I couldn’t break my work down into slices <a target="_blank" href="https://dileeparanawake.com/minimum-viable-slice">see my post on Minimum Viable Slice</a> because I couldn’t tell where one concern ended and another began. Was this part of the UI? The backend? Should I validate input in the handler or the service? I didn’t know — and it made every step feel uncertain.</p>
<p>It felt like having pieces of a jigsaw without the picture on the front of the box to guide me.</p>
<p>Pausing to think about the system helped me:</p>
<ul>
<li><p>Clarify what was <em>actually</em> in my tech stack — and what purpose each piece served</p>
</li>
<li><p>Understand how tools like Zod, JWT, and Drizzle mapped onto architectural concerns like validation, auth, and persistence</p>
</li>
<li><p>Iterate more confidently on my scope and design — because I now had somewhere to put things</p>
</li>
</ul>
<h2 id="heading-what-i-did-creating-a-mental-model-of-my-application">What I Did: Creating a Mental Model of My Application</h2>
<p>The first thing I did was map out the major concerns: UI, API, validation, authentication, business logic, persistence.</p>
<p>Then I walked through a single flow: a user submits a prompt. What happens from there?</p>
<p>I traced the data as it moved from the frontend, through the route handler, into validation, through services, to the database, and back.</p>
<p>This is how I thought about concerns, components, and responsibilities:</p>
<ul>
<li><p>What are the <strong>main concerns</strong> (broad areas of responsibility)?</p>
</li>
<li><p>What are the <strong>main components</strong> (things that fulfil those concerns)?</p>
</li>
<li><p>What are their <strong>responsibilities</strong> (the job they do)?</p>
</li>
</ul>
<h3 id="heading-heres-what-i-came-up-with">Here's what I came up with:</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Concern</td><td>Component</td><td>Responsibility</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Presentation/UI</strong></td><td>Frontend (Next.js)</td><td>Handle routing, form input, display responses &amp; history</td></tr>
<tr>
<td><strong>Business Logic/API</strong></td><td>Backend API Routes (Next.js)</td><td>Validate input, handle auth, call OpenAI, access DB</td></tr>
<tr>
<td><strong>Persistence</strong></td><td>PostgreSQL + Drizzle ORM</td><td>Store user data, prompt/response history</td></tr>
<tr>
<td><strong>AI Integration</strong></td><td>OpenAI API</td><td>Generate responses from prompt</td></tr>
<tr>
<td><strong>Authentication</strong></td><td>Google OAuth (via Auth.js)</td><td>Login flow, user identity verification</td></tr>
<tr>
<td><strong>Session/Auth</strong></td><td>JWT Layer</td><td>Issue/verify user tokens for protected routes</td></tr>
<tr>
<td><strong>Security</strong></td><td>Rate Limiting &amp; Input Validation</td><td>Prevent abuse, ensure request safety</td></tr>
<tr>
<td><strong>Environment/Secrets</strong></td><td>.env Config</td><td>Manage API keys and sensitive settings</td></tr>
<tr>
<td><strong>Deployment</strong></td><td>Docker &amp; DigitalOcean</td><td>Containerized deployable app instance</td></tr>
<tr>
<td><strong>CI/CD</strong></td><td>GitHub Actions (optional)</td><td>Automate test &amp; deploy pipeline (optional for portfolio)</td></tr>
</tbody>
</table>
</div><p>This helped me understand not just what tools I had, but why each one existed and what part of the system it served.</p>
<p>By following the flow through the system, I started to see that architecture wasn’t abstract or static — it was dynamic and revealing. I began to notice gaps, assumptions, and flaws in my knowledge.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1752220477848/80848075-afc7-41b1-ac24-9c7b243281e1.png" alt="system architecture diagram" class="image--center mx-auto" /></p>
<p>This wasn’t just an academic exercise. It helped me understand:</p>
<ul>
<li><p>What a “concern” really is, and how to separate them</p>
</li>
<li><p>The difference between a domain (like "auth") and a component (like <code>AuthForm.tsx</code>)</p>
</li>
<li><p>What boundaries exist in my system — and why those boundaries matter</p>
</li>
</ul>
<p>Up until then, architecture felt like a checkbox — something you’re told to sketch out at the start of a project, but not something grounded in understanding or learning. Then I came across Martin Fowler’s definition of architecture as a 'shared understanding of the system’s design' — and things clicked. In this case, the shared understanding was just between me and myself — but that alone was transformative.</p>
<blockquote>
<p><em>“Architecture is the shared understanding of the system's design.” — Martin Fowler</em></p>
</blockquote>
<h2 id="heading-what-i-learned-from-vague-tools-to-clear-responsibilities">What I Learned: From Vague Tools to Clear Responsibilities</h2>
<p>Once I saw how the system worked, I started to understand where each tool fit — and why it was there.</p>
<p>For example:</p>
<ul>
<li><p><strong>Zod</strong>: a runtime validator that ensures incoming data (like API requests) matches the expected shape. While TypeScript interfaces define static types at compile time, Zod performs validation at runtime — so your app can safely use the data it receives.</p>
</li>
<li><p><strong>TypeScript interfaces</strong>: static contracts that describe the expected shape of data. They help during development but aren't enforced at runtime.</p>
</li>
<li><p><strong>JWT</strong>: a signed token (not middleware itself) that acts like a passport — proving the user's identity and authorisation. It's typically verified by auth middleware, which allows or blocks access to protected routes and services.</p>
</li>
<li><p><strong>Drizzle ORM</strong>: a type-safe way to define your database schema and query your DB in TypeScript — removing the need for raw SQL in most cases.</p>
</li>
</ul>
<p>I stopped seeing tools as magic boxes. I started seeing them as components in a larger system — each with a role and a place.</p>
<p>This also exposed gaps. For example, I realised I didn’t fully understand how OAuth and JWT worked together. But now that I knew <em>where</em> in the flow that gap lived, I could go learn it in context.</p>
<p>And that’s the difference. I wasn’t just Googling things — I was learning how they fit.</p>
<h2 id="heading-why-it-helped-i-can-now-approach-the-build-with-more-confidence">Why It Helped: I Can Now Approach the Build with more Confidence</h2>
<p>I haven’t written the core feature code yet — but I now know what I’m building, how I can approach the design, and how to break it down into smaller, logical parts using my Minimum Viable Slice plan.</p>
<p>The architecture will evolve — and that’s fine. But now:</p>
<ul>
<li><p>I understand where different types of logic belong</p>
</li>
<li><p>I know what my first slice should include</p>
</li>
<li><p>I can scope, plan, and test without guessing</p>
</li>
<li><p>And when something breaks later, I’ll have a better idea of <em>where</em> in the system to look</p>
</li>
</ul>
<p>This isn’t about perfection. It’s about moving from fog to flow — slowly, but intentionally.</p>
<h2 id="heading-is-this-overthinking-or-useful">Is This Overthinking or Useful?</h2>
<p>Honestly, I worried I might be overthinking it. Shouldn’t I just be building things and figuring it out as I go?</p>
<p>But over time, I’ve come to see that this <em>is</em> part of building. Designing isn’t procrastination — it’s what gives the build direction.</p>
<p>Having a rough architectural map gave me clarity, helped me learn more deeply, and will save me time and confusion later. Especially as a junior developer, this kind of deliberate thinking can be a force multiplier.</p>
<p>Even now, I may still get stuck — but I’ll be stuck in a known place, not lost in a maze.</p>
<h2 id="heading-advice-to-other-juniors-map-before-you-build">Advice to Other Juniors: Map Before You Build</h2>
<p>If you're starting a project and feel stuck, try stepping back.</p>
<p>Ask yourself:</p>
<ul>
<li><p>How does a request move through your system?</p>
</li>
<li><p>What part handles validation?</p>
</li>
<li><p>What’s responsible for business logic?</p>
</li>
<li><p>Where should this data end up?</p>
</li>
</ul>
<p>Drawing a rough architecture diagram, walking through a single flow, and identifying where each tool or function lives can be a huge unlock.</p>
<p>These diagrams don’t need to be perfect or pretty — they just need to help you reason through what you're building.</p>
<p>Still not sure whether your planning is helpful or just a detour? Here’s a quick test I found useful:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Question</td><td>If yes…</td><td>If no…</td></tr>
</thead>
<tbody>
<tr>
<td>1. Does it help clarify your system?</td><td>✅ Keep it</td><td>⚠️ Too abstract</td></tr>
<tr>
<td>2. Does it help break things into shippable slices?</td><td>✅ Useful</td><td>⚠️ Planning black hole</td></tr>
<tr>
<td>3. Is it scoped to current work (e.g. MVS1)?</td><td>✅ Lean</td><td>⚠️ Premature abstraction</td></tr>
<tr>
<td>4. Is it helping you learn and make decisions?</td><td>✅ Productive</td><td>⚠️ Risk of procrastination</td></tr>
</tbody>
</table>
</div><p>This helped me validate that I wasn’t just stalling — I was preparing to build well.</p>
<p>You don’t need to be an architect to think architecturally. You just need to care how your system fits together.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>This reflection doesn’t mean I’ve figured it all out — far from it.</p>
<p>But it was the first time I stopped just writing code, and started thinking like an engineer.<br />Building moved beyond just code.</p>
<p>Looking ahead, as AI becomes more integrated into our day-to-day tools, I think we’ll need to be less like coders, and more like engineers — even at a junior level.</p>
<p>And right now, using system design and architecture to uncover gaps in my knowledge and build with more intent feels like the right next step.</p>
<p>For me, that shift made all the difference.</p>
<h2 id="heading-whats-next">What’s Next?</h2>
<p>I’ll use this blog to share what I’m learning, document side projects, reflect on my journey into software engineering and to share my thoughts on the industry.</p>
<p>You can sign up for <a target="_blank" href="https://dileeparanawake.com/newsletter">email updates here</a>, <a target="_blank" href="https://github.com/dileeparanawake">follow my GitHub</a>, or connect with me on <a target="_blank" href="https://www.linkedin.com/in/dileepa-ranawake/">Linkedin</a> as I build stuff 🙂</p>
<hr />
<p><strong>Dileepa Ranawake</strong> is a full-stack software engineer, accessibility advocate, and former startup co-founder. He’s exploring how AI, accessibility, and engineering can come together to build tech-for-good.</p>
]]></content:encoded></item><item><title><![CDATA[Minimum Viable Slice: Overcoming Builder’s Block]]></title><description><![CDATA[Ever started a new project and wondered: “Where do I even begin?”
That was where I was at, in and during my Jammming project on my GitHub. I knew what I wanted to build but once I got into the code, it all started to unravel. I jumped between compone...]]></description><link>https://dileeparanawake.com/minimum-viable-slice</link><guid isPermaLink="true">https://dileeparanawake.com/minimum-viable-slice</guid><category><![CDATA[architecture]]></category><category><![CDATA[full stack]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Product Management]]></category><category><![CDATA[Technical Design]]></category><dc:creator><![CDATA[Dileepa Ranawake]]></dc:creator><pubDate>Fri, 04 Jul 2025 17:35:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751649769229/c281ec77-04b6-4077-8757-85bdf36089c0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-ever-started-a-new-project-and-wondered-where-do-i-even-begin">Ever started a new project and wondered: “Where do I even begin?”</h2>
<p>That was where I was at, in and during my <a target="_blank" href="https://github.com/dileeparanawake/Jammming">Jammming project</a> on my GitHub. I knew what I wanted to build but once I got into the code, it all started to unravel. I jumped between components. Refactored things I hadn’t even finished. Tried to connect backend endpoints before the frontend was working. It became chaotic, and became slow. Bugs were hard to track. I didn't know where to begin. In the build phase it was all a bit chaotic. In fairness to myself, I was figuring out react, but it got me thinking about how I'd approach the process in the future.</p>
<p>With my background in startups / being a product manager I'm familiar (like you probably are) with agile methodology, the learn Build-Measure-Learn cycle. In particular I was used to thinking about an MVP in desiging products.</p>
<p>But when it came to building, MVP thinking didn’t help. It told me to build a "skateboard before the car"—but how does that translate to actual code how we tackle a project? If you're not super geared up with design patterns and architecture it's quite difficult to usefully break down an MVP.</p>
<p>If you're in product you're probably familiar with the challenge of measuring meaningful progress without bugging your engineers. With my engineer hat on, the limits of MVPs are clear.</p>
<p>To overcome the where can I start problem, In this post, I’ll share the idea of the <strong>Minimum Viable Slice</strong>—a small, deployable, technically complete unit of functionality that helped me break through that builder’s block and start making confident progress.</p>
<h2 id="heading-the-problem-with-mvp-thinking">The Problem with MVP Thinking</h2>
<p>We’ve all heard it: <em>start with the simplest version of your product</em>. And yes, Minimum Viable Product (MVP) thinking is great from a business and user perspective. But when you're the one writing the code, “simplest product” doesn’t always give you a clear place to start.</p>
<p>In practice, it left me floundering. What should I code first? Should I build the UI? The database? Some mocked-up flow?</p>
<p>What ended up happening was:</p>
<ul>
<li>I jumped between frontend and backend</li>
<li>I introduced bugs by half-implementing features</li>
<li>I rewrote logic as the scope shifted</li>
<li>I found it hard to test anything in isolation</li>
</ul>
<p>The result was a messy codebase, lots of rewrites, and slow progress.</p>
<h2 id="heading-what-is-a-minimum-viable-slice">What Is a Minimum Viable Slice?</h2>
<p>A <strong>Minimum Viable Slice (MVS)</strong> is my antidote to MVP ambiguity. It’s a narrow, technical-first way to scope and build your project incrementally.</p>
<p>Here’s how I define a slice:</p>
<ul>
<li><strong>Mergeable</strong> – a slice can be safely merged into main without breaking the system</li>
<li><strong>Testable</strong> – its core function can be verified (input → output) through tests or basic interaction</li>
<li><strong>Functional</strong> – it delivers a clear technical goal (e.g. auth, persistence, or an API integration)</li>
</ul>
<p>A slice can be designed to be mergeable to <code>main</code>, deployable to a staging environment, or even production-ready—depending on your goal. The point is that it’s internally stable and complete, even if it’s not yet user-facing. Ultimately, I think of MVS as more of a guiding principle than a rigid set of rules.</p>
<p>It’s <em>not</em> style-focused.
It does <em>not</em> need to be user-facing.
It’s <em>not</em> concerned with polish.</p>
<p>The focus is just something that's technically functional and deployable,</p>
<p>Each slice stands on its own and pushes your architecture and build forward.</p>
<h2 id="heading-how-mvs-helped-me-scope-my-llm-app">How MVS Helped Me Scope My LLM App</h2>
<p>After the Jammming experience, I wanted to address some of the builders block / pitfalls in my next project - a full-stack LLM app.</p>
<p>Whereas in Jammming I did a sketch and went all out, I decided to create a hybrid scope / technical design document.</p>
<p>In the technical scope defined my requirements, problem etc and then I broke the app down into slices. It's still a work in progress and this post is reflecting on my learnings / things and research but at time of writing, at a high level it looked like this:</p>
<blockquote>
<ol>
<li><strong>Basic LLM Prompt Loop (No Auth)</strong> - Set up a functional prompt → response loop using the OpenAI API, in a Dockerized full-stack app.</li>
<li><strong>User Authentication</strong> - Introduce user authentication to enable personalised features and secure user-specific routes in later phases.</li>
<li><strong>Prompt History &amp; Persistence</strong> - Add database-backed storage of user prompts and LLM responses, enabling a personal history view for logged-in users.</li>
<li><strong>Milestone Guidance via System Prompting</strong> - Use system prompts to generate simple milestone guidance based on prior user input, without external knowledge grounding.</li>
</ol>
</blockquote>
<p>Each slice was designed to be technically complete enough to be merged and tested as part of the main project. For example, I could implement and validate the LLM prompt loop without needing auth or persistence yet. The slices aren't standalone services—they’re functional steps that fit together into a cohesive build.</p>
<p>This way, I could:</p>
<ul>
<li>Make real progress (each slice was visible and testable)</li>
<li>Focus on technical design early</li>
<li>Avoid rewrites by building modularly</li>
<li>Keep motivation high (tiny wins add up!)</li>
</ul>
<p>I’ll try to share a snapshot of my slice plan once I publish the repo.</p>
<h2 id="heading-mvs-vs-vertical-slice-architecture-vs-mvp">MVS vs Vertical Slice Architecture vs MVP</h2>
<p>You might be thinking: isn't this just vertical slice architecture?</p>
<p>Sort of — but with a twist.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Concept</td><td>Focus</td><td><em>Typical Use</em></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Vertical Slice Architecture</strong></td><td>Feature-oriented: full stack per feature</td><td>Layered monoliths, clean architecture</td></tr>
<tr>
<td><strong>Thin Slice MVP</strong></td><td>User-facing smallest product value</td><td>Agile product teams</td></tr>
<tr>
<td><strong>Minimum Viable Slice</strong></td><td>Technical scope, user-facing optional</td><td>Solo devs, junior engineers, breaking your build down</td></tr>
</tbody>
</table>
</div><h2 id="heading-common-pitfalls-to-avoid">Common Pitfalls to Avoid</h2>
<ul>
<li><strong>Tightly Coupled Slices</strong>: If one slice breaks when another changes, it defeats the point of isolating functionality.</li>
<li><strong>Vague Interfaces</strong>: Without clear input/output definitions, later slices may cause conflicts or rewrites.</li>
<li><strong>Backend Bias</strong>: It’s tempting to over-focus on infrastructure. Include minimal UI views early to maintain usability context.</li>
<li><strong>Skipping Tests</strong>: Tests give you confidence that each slice is safe to merge. Even basic checks help.</li>
<li><strong>Endless Refactoring</strong>: Log ideas for improvements, but move forward slice by slice. Avoid reworking every layer with each new addition.</li>
</ul>
<p>MVS is about building up the infrastructure and logic <strong>before</strong> focusing on polished, user-facing experiences.</p>
<p>It gives you a structure to think and build incrementally—even when you're working solo or figuring things out as you go.</p>
<h2 id="heading-when-to-use-it-and-how-to-start">When to Use It and How to Start</h2>
<p>I’ve found MVS especially helpful in these scenarios:</p>
<ul>
<li>Building side projects or learning apps</li>
<li>Rapid prototyping (e.g. hackathons)</li>
<li>Scoping complex systems without overwhelm</li>
<li>Feeling builders block at the start of a project</li>
</ul>
<p>Here’s how I recommend getting started:</p>
<ol>
<li><strong>Define the core purpose of your project</strong></li>
<li><strong>Write down the technical concerns or building blocks</strong></li>
<li><strong>Break those into “slices” that can be isolated and deployed</strong> - try and design slices so that minimal refactoring is required for subsequent slices.</li>
<li><strong>Order them logically—start with what other pieces depend on</strong> - this informs your architecture and details.</li>
<li><strong>Deploy and test each slice before moving on</strong></li>
</ol>
<p>This shifts your thinking from <em>“what features do I need?”</em> to <em>“what system pieces do I need to see working?”</em> Another way of thinking about it is you are building internal features / system features.</p>
<h2 id="heading-lessons-learned-and-whats-next">Lessons Learned and What’s Next</h2>
<p>Since using this approach, I’ve felt more clarity and less frustration in starting a project / planning.</p>
<p>This has also got me thinking more about interface design, and the gaps in my knowledge there. I might do a deep dive onto interface design later / share my learnings.</p>
<p>I’m currently using this method to guide my LLM web app build—and I’ll be publishing the full project, plan soon.</p>
<p>I'll test this approach, so if I find issues with this approach I'll try write about it and share my learnings... stay tuned for updates!</p>
<h2 id="heading-summary">Summary</h2>
<p>A Minimum Viable Slice doesn’t replace MVP or vertical slice architecture— it complements them. It gives you a <em>practical way to start</em> building things without getting overwhelmed.</p>
<p>It’s deployable, testable, and keeps your project moving.</p>
<p>It gives you a way to break things up logically.</p>
<p><strong>Have you tried scoping your projects like this?</strong>  </p>
<p>Connect with me on <a target="_blank" href="https://www.linkedin.com/in/dileepa-ranawake">LinkedIn</a> or check out my <a target="_blank" href="https://github.com/dileeparanawake">GitHub</a>. I’d love to hear how you approach scoping and breaking down projects and thinking about architecture and design.</p>
<h2 id="heading-whats-next">What’s Next?</h2>
<p>I’ll use this blog to share what I’m learning, document side projects, reflect on my journey into software engineering and to share my thoughts on the industry.</p>
<p>You can sign up for <a target="_blank" href="https://dileeparanawake.com/newsletter">email updates here</a> or <a target="_blank" href="https://github.com/dileeparanawake">follow my GitHub</a> as I build stuff 🙂</p>
]]></content:encoded></item><item><title><![CDATA[How I Built My Developer Blog: Tools, Trade-offs, and Lessons Learned]]></title><description><![CDATA[Why I Wanted a Developer Blog
As my career in software engineering evolves, I wanted a space to:

Share learnings.
Reflect on my side projects.
Share my industry thoughts and views.
Build out my profile.
Low cost / free.

I already had a Substack, bu...]]></description><link>https://dileeparanawake.com/how-i-built-my-developer-blog-tools-trade-offs-and-lessons-learned</link><guid isPermaLink="true">https://dileeparanawake.com/how-i-built-my-developer-blog-tools-trade-offs-and-lessons-learned</guid><category><![CDATA[blog]]></category><category><![CDATA[Hashnode]]></category><category><![CDATA[Bootstrap]]></category><category><![CDATA[substack]]></category><category><![CDATA[Astro]]></category><category><![CDATA[General Advice]]></category><dc:creator><![CDATA[Dileepa Ranawake]]></dc:creator><pubDate>Wed, 25 Jun 2025 16:15:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751533702627/2654df03-ac47-4332-a7ef-cd6c693fe809.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-why-i-wanted-a-developer-blog">Why I Wanted a Developer Blog</h2>
<p>As my career in software engineering evolves, I wanted a space to:</p>
<ul>
<li>Share learnings.</li>
<li>Reflect on my side projects.</li>
<li>Share my industry thoughts and views.</li>
<li>Build out my profile.</li>
<li>Low cost / free.</li>
</ul>
<p>I already had a <a target="_blank" href="https://substack.dileeparanawake.com/">Substack</a>, but hadn’t used it much and it was a bit unfocused (I started my substack a year ish ago as a kind of catch all blog to share broad thoughts).</p>
<p>It was easy to get started on Substack, but I found its push toward monetisation a bit off-putting (I don’t want to paywall my content). I wanted a clean, developer-focused blog, without aggressive paywalls and email capture and a bit more customisability.</p>
<p>So I started exploring other options...</p>
<h2 id="heading-considering-static-sites-and-github-pages">Considering Static Sites and GitHub Pages</h2>
<p>I wanted to keep it simple, and keep it low cost! A static site hosted via GitHub Pages is free, fast, and version-controlled.</p>
<p>After a bit of searching I found <strong><a target="_blank" href="https://getbootstrap.com/">Bootstrap</a></strong> and found a lot of nice, <a target="_blank" href="https://html5up.net/">free templates</a>, and started tinkering...</p>
<h3 id="heading-what-i-liked">What I liked:</h3>
<ul>
<li>Simple to set up</li>
<li>Great for a no-frills landing page</li>
<li>Loads of community templates</li>
</ul>
<h3 id="heading-what-i-didnt">What I didn’t:</h3>
<ul>
<li>I could see routing and organising posts becoming a pain quickly.</li>
<li>Didn't want to spend time writing boilerplate or patching together workarounds.</li>
<li>I could see it as easy to outgrow for more complex content e.g filtering tags, components, routing etc.</li>
</ul>
<p>This made me stop and ask: what do I really need?</p>
<h2 id="heading-step-2-defining-my-requirements">Step 2: Defining My Requirements</h2>
<p>Roughly, I needed:</p>
<ul>
<li>A standard blog layout (posts, tags, nav)</li>
<li>Markdown support</li>
<li>Easy updates and routing</li>
<li>Custom domain</li>
<li>Low or no cost</li>
</ul>
<p>I didn’t want to spend too much time maintaining infrastructure, but something a bit more powerful and flexible, that could accommodate more complex and dynamic elements</p>
<h2 id="heading-step-3-trying-astro">Step 3: Trying Astro</h2>
<p>Astro caught my eye. It’s lightweight, fast, and developer-friendly. I built a minimal Astro blog, deployed it via GitHub Pages using GitHub Actions. Quite fun. Easy. I avoided something like Next.js + Vercel - simply for cost and rendering speed (and maybe I was a little lured by new and shiny)...</p>
<p>I built a little Astro blog (not a looker!) to familiarise myself with the framework, test out GitHub actions and deploy to pages and Astro basics. You can see my GitRepo <a target="_blank" href="https://github.com/dileeparanawake/astro-blog">here</a> and the live site <a target="_blank" href="https://dileeparanawake.github.io/astro-blog">here</a>. If it was my main production site I'd have started with their <a target="_blank" href="https://astro.new/latest/">free templates</a>, to avoid wasting time on styling etc...</p>
<p><a target="_blank" href="https://github.com/dileeparanawake/astro-blog"><img src="https://raw.githubusercontent.com/dileeparanawake/astro-blog/main/astro_Screenshot.png" alt="my simple astro blog" /></a></p>
<h3 id="heading-what-i-liked-1">What I liked:</h3>
<ul>
<li>Nice <a target="_blank" href="https://docs.astro.build/en/tutorial/0-introduction/">getting started guide</a></li>
<li>Fast static generation</li>
<li>Easy GitHub deployment</li>
<li>Markdown-friendly</li>
<li>More flexibility than Bootstrap - eg more powerful links, react like components for things like nav bars, markdown support to easily turn a folder of posts into a cms like sortable post structure.</li>
<li>Lots of <a target="_blank" href="https://astro.new/latest/">free templates</a></li>
</ul>
<h3 id="heading-what-i-didnt-1">What I didn’t:</h3>
<ul>
<li>No native newsletter or analytics.</li>
<li>Required a bit more wiring up (e.g., custom routing, metadata)</li>
<li>Still felt like I’d be spending time tweaking instead of writing, especially when I added up my requirements (analytics - quite easy via Google, newsletters etc).</li>
</ul>
<p>If I was building a custom blog or had time to spare, Astro would be a great choice - I could see it being fun, powerful and connecting something like PostHog, for more privacy centric analytics.</p>
<h2 id="heading-step-4-substack-again-almost">Step 4: Substack Again? Almost...</h2>
<p>I briefly considered going back to Substack—its built-in email support was tempting—but the paywall-first model, low customisability, broad focus, didn’t feel right for what I wanted.</p>
<h2 id="heading-step-5-finding-hashnode">Step 5: Finding Hashnode</h2>
<p>Eventually, I discovered <strong>Hashnode</strong>. It ticked nearly all my boxes:</p>
<ul>
<li>Custom domain support</li>
<li>Free to use</li>
<li>Newsletter integration</li>
<li>Built-in analytics</li>
<li>Developer-focused (Markdown, GitHub embeds, etc.)</li>
<li>No hard sell on monetisation</li>
<li>Customisable for the future, but easy to set up.</li>
<li>Developer focused community</li>
<li>Good for SEO</li>
</ul>
<p>It felt like the right middle ground: more power and polish than Substack, less setup than Astro or Bootstrap... and importantly I wanted to allocate more time to building cool things... so I took the leap, got on Hashnode and published my first post (this one!).</p>
<p>Ultimately, I learned that clear requirements—even for personal projects—can save a lot of time building, researching, and second-guessing.</p>
<h2 id="heading-lessons-learned">Lessons Learned</h2>
<p>I approached this like a real project: defining requirements, prototyping, testing tools, iterating, refining my requirements and deciding what to use based on value vs. effort.</p>
<p><strong>Here's my learning summary:</strong></p>
<ul>
<li><strong>Specs matter</strong>: even for personal projects, define your requirements early. It’ll save you time and indecision later. Specs are always a little emergent in that discovery phase, but it pays to define the key outcomes, upfront (as well as you can).</li>
<li><strong>Bootstrap is solid</strong> for simple landing pages, but not ideal for content-heavy blogs.</li>
<li><strong>Astro is brilliant</strong>, especially if you want to build something fully custom and fast.</li>
<li><strong>Substack is great if you're newsletter/ paywall first</strong>, want a broader audience **but not if you want control.</li>
<li><strong>Hashnode hits the sweet spot</strong> for developer blogs with minimal setup and good features.</li>
</ul>
<h2 id="heading-whats-next">What’s Next</h2>
<p>I’ll be using this blog to share what I’m learning, document side projects, and reflect on my journey into software engineering, sharing my thoughts on the industry.</p>
<p>You can <strong><a target="_blank" href="https://dileeparanawake.com/newsletter">sign up for email updates here</a></strong> or <a target="_blank" href="https://github.com/dileeparanawake">follow my GitHub</a> as I build stuff 🙂</p>
]]></content:encoded></item></channel></rss>