<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pasindu Balasooriya</title>
    <description>The latest articles on DEV Community by Pasindu Balasooriya (@pasindu_balasooriya).</description>
    <link>https://dev.to/pasindu_balasooriya</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3991950%2F9a1e1942-86c8-4fdb-b0e2-7bad50356420.jpg</url>
      <title>DEV Community: Pasindu Balasooriya</title>
      <link>https://dev.to/pasindu_balasooriya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pasindu_balasooriya"/>
    <language>en</language>
    <item>
      <title>Heartbeats, Not Clicks: UX Under Extreme Pressure</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Tue, 08 Sep 2026 12:15:27 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/heartbeats-not-clicks-ux-under-extreme-pressure-4bmi</link>
      <guid>https://dev.to/pasindu_balasooriya/heartbeats-not-clicks-ux-under-extreme-pressure-4bmi</guid>
      <description>&lt;p&gt;When UX Is a Matter of Seconds: Designing for High-Stakes Interfaces&lt;/p&gt;

&lt;p&gt;In high stakes interface design, the difference between a good user experience and a failed one is not measured in clicks or conversions. It is measured in heartbeats. Whether you are designing for a cockpit, a surgical suite, or an emergency command center, your users are operating at the edge of human cognitive capacity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdqkdkh7g95mkxmv5q2mr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdqkdkh7g95mkxmv5q2mr.png" alt=" " width="720" height="731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why standard UX breaks under pressure
&lt;/h2&gt;

&lt;p&gt;Most modern UX is built for leisurely immersion. We want users to explore, compare, and discover. But in high-stress environments, the user’s mental model shifts to interruption-based action. The interface becomes a cognitive prosthetic.&lt;/p&gt;

&lt;p&gt;The question is not “How delightful is this?” The question is “How quickly can a human do the right thing under stress?”&lt;/p&gt;

&lt;p&gt;A quick example: emergency dispatch&lt;/p&gt;

&lt;p&gt;Imagine a dispatcher who has 30 seconds to get an ambulance moving. A traditional layout shows a rich record view, filters, and status tabs. In a high-stakes layout, the screen leads with the address, incident type, and a single “dispatch” action. Details sit one tap away, so the critical path stays clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  1) The logic of chaos: Hierarchical Task Analysis
&lt;/h2&gt;

&lt;p&gt;Before the first wireframe, perform a digital autopsy of the task using Hierarchical Task Analysis (HTA). Standard flows show the path, but HTA reveals the why and breaks the objective into sub-goals.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Identify cognitive bottlenecks where complexity overwhelms working memory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Map sequence pressure so deep-work screens prioritize findability and wearables prioritize glanceability.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2) The math of survival: Fitts’s and Hick’s laws
&lt;/h2&gt;

&lt;p&gt;In high-pressure design, mathematical models are predictive tools for human performance.&lt;/p&gt;

&lt;p&gt;Fitts’s Law states that the time to acquire a target is a function of distance and size:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Place primary actions where the cursor naturally lands after a scan (often the bottom-right on desktop).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On mobile or wearable devices used in motion, expand targets to the edges to reduce miss-taps.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hick’s Law predicts that decision time increases with the number of choices:&lt;/p&gt;

&lt;p&gt;In a crisis, reduce $n$ to the minimum that still keeps people safe. Fewer options mean less time spent thinking and more time acting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg5531ydnyc88ldwzz0zh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg5531ydnyc88ldwzz0zh.png" alt=" " width="720" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3) Protecting working memory
&lt;/h2&gt;

&lt;p&gt;Cognitive Load Theory reminds us that the brain can only process a limited amount of information at once. When that limit is reached, you get a bottleneck.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Progressive disclosure: hide secondary details behind explicit triggers so the primary task stays clean.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The 8pt grid: a strict grid creates predictable rhythm. In a crisis, predictability is the antidote to panic.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4) The “shrunken website” fallacy
&lt;/h2&gt;

&lt;p&gt;The biggest mistake in high-stakes design is shrinking a desktop dashboard to fit a watch face.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Desktop users are immersed and can handle density.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wearable users are interrupted and need a flattened hierarchy and linear controls.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion: designing for survival
&lt;/h2&gt;

&lt;p&gt;High-stakes interface design is not about aesthetics. It is about the physics of human cognition. When we apply HTA, Fitts’s Law, and Hick’s Law, we do not just build better apps. We build systems that protect human performance when it matters most.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Design for interruption, not exploration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use HTA to expose cognitive bottlenecks early.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Minimize choices to cut decision time under stress.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Favor glanceability on wearables and findability on deep-work screens.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>design</category>
      <category>software</category>
      <category>ux</category>
    </item>
    <item>
      <title>ACID vs BASE is a false dichotomy: Why your system needs both</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Tue, 08 Sep 2026 12:09:12 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/acid-vs-base-is-a-false-dichotomy-why-your-system-needs-both-59h8</link>
      <guid>https://dev.to/pasindu_balasooriya/acid-vs-base-is-a-false-dichotomy-why-your-system-needs-both-59h8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rc1tcqjszsd6a9fr9jk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rc1tcqjszsd6a9fr9jk.png" alt=" " width="720" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The architect’s dilemma
&lt;/h2&gt;

&lt;p&gt;If you have ever designed a distributed system, you have faced the classic dilemma. On one side are the ACID principles with all that they bring, Atomicity, Consistency, Isolation, and Durability. This is the world of banks and monetary transactions, with tight security where data integrity is of paramount importance and every transaction guarantees meticulous precision. But this strong consistency comes at the cost of performance.&lt;/p&gt;

&lt;p&gt;On the other side you have BASE (Basically Available, Soft State, Eventually Consistent). This is the world of social media feeds and high traffic websites with high availability and scalability. Here, strong consistency is reduced to eventual consistency where data will be consistent with comparable latency.&lt;/p&gt;

&lt;p&gt;For years, architects have been told they must choose but one at the expense of the other. It is a compromise that feels like you are leaving value on the table either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hybrid solution
&lt;/h2&gt;

&lt;p&gt;A mature modern architecture should not force you to pick just one. Instead, it should recognize a platform with its entire complexity that an application is not a singular entity, rather, it’s a collection of different jobs. For instance, a payment service has different needs than a product catalog, as does an inventory service compared to a user profile service.&lt;/p&gt;

&lt;p&gt;The solution to this, ideally in a distributed system with a microservices architecture, is to adopt a hybrid data strategy. Instead of a “one size fits all approach”, we can apply different data models to different parts of the application based on their requirement and business logic. We can achieve both strong consistency and high availability where they are needed in the microservices. This allows us to achieve perfect data integrity for critical operations while simultaneously delivering the high availability and performance the users expect for less critical operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real-world blueprint on architecting for a global retailer
&lt;/h2&gt;

&lt;p&gt;Consider a mid-sized global apparel company with branches in Europe, Asia and America and E-commerce presence in those continents, whose rapid growth outpaced their IT infrastructure, leading to a classic conflict between operational needs and customer experience.&lt;/p&gt;

&lt;p&gt;They were facing two different demands, one no less important than the other.&lt;/p&gt;

&lt;p&gt;1.Inventory accuracy&lt;/p&gt;

&lt;p&gt;When a customer buys a pair of jeans online, the inventory system must be 100% accurate immediately across the globe. An error here could mean overselling stock, leading to order cancellations, with customer dissatisfaction and logistical catastrophes following soon after. This part of the business hence demands perfect precision.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fast and engaging storefront&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At the same time, customers browsing the website expect a fast, seamless experience. They would want to see product catalogs load instantly and their user profiles must be responsive all the time. This part of the business demands high availability and top-tier performance.&lt;/p&gt;

&lt;p&gt;This is where a single, monolithic database begins to struggle. Forcing the entire system to operate with strict rules of the inventory makes the storefront less responsive just as well as letting the whole system abiding by the eventual consistency principles risks the integrity of financial and stock data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcd4whoy8xed0czj9o4f8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcd4whoy8xed0czj9o4f8.png" alt=" " width="720" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First let us talk about the parts of the business that necessitate zero margin for errors, like inventory, orders and payments. This is the financial and logistical backbone of the company where an error is not just a bug, it’s revenue loss and customer dissatisfaction. The primary goal of such services is strong consistency. To this end, we use a synchronous multi-master replication strategy, a diplomatic choice that introduces both safety and reliability.&lt;/p&gt;

&lt;p&gt;Now let’s break that down.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Synchronous&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a transaction is processed, the transaction is not confirmed until every single regional database/node has received the update and acknowledged it. This guarantees strong consistency and is ACID-compliant. For instance, the inventory count would always be accurate across the globe at any given moment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-master&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any regional node can accept writes (e.g., processing an order). This provides high availability and eliminates the single point of failure while ensuring that an outage in a regional node does not stop the entire company from functioning.&lt;/p&gt;

&lt;p&gt;This approach is the digital equivalent of a bank transaction, built on ACID principles. To implement this, we could use a battle-tested relational database like PostgreSQL or a managed cloud service designed for the exact same purpose as Amazon Aurora. The trade-off is slight increase in write time but it’s a price worth paying to ensure security and accuracy.&lt;/p&gt;

&lt;p&gt;Next, let’s talk about parts of the business that interact with customers directly like product catalogs, user profiles and recommendations. The primary goal of these services is to be fast, engaging and always online. To this end, we use an asynchronous, master-slave replication strategy.&lt;/p&gt;

&lt;p&gt;Here’s what that means.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asynchronous&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a user updates their profile, the change is written to a primary master node, and a success message is immediately returned to the application layer. The update is then propagated to the rest of the read replicas in the background, without making the user wait (reduced latency).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Master-slave&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single master database handles all writes, ensuring a simple, conflict-free process. Meanwhile, read-only slave replicas in other regions handle read requests that improves read speed and load balancing of read requests.&lt;/p&gt;

&lt;p&gt;This approach leads to eventual consistency and is BASE-compliant. There might be a delay of few seconds before a change is globally visible which is an acceptable trade-off for a highly responsive platform. To implement this, we could use a database like MongoDB or a highly scalable cloud-based NoSQL database like Amazon DynamoDB or Cassandra.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key takeaway — design with intent
&lt;/h2&gt;

&lt;p&gt;For years, the debate between ACID and BASE has been viewed as a battle of principle. But as we have seen, the most effective solution is not about choosing a side, and it is not always about dichotomy but holism every now then. It involves recognizing that a complex system is not a monolith with a single set of needs but rather a diverse ecosystem with different business logic and functions with unique requirements.&lt;/p&gt;

&lt;p&gt;The inventory system’s job is to be an infallible ledger while the product catalog’s job is to be fast and engaging. By using a hybrid strategy, we allow each part of our system to be excellent at its specific task, rather than forcing the entire system to settle for a “one size fits all” compromise.&lt;/p&gt;

&lt;p&gt;Thus, the next time you are faced with architect’s dilemma, remember to design with intent. Analyze the business domain, understand the trade-offs and build a system that uses the right strategy for the right use case. The most resilient of systems do not choose a side. Instead, they build a bridge between them.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>scalability</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Dynamic Typing Is Fun, Until It Isn’t (Lessons from Python and C#)</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Tue, 08 Sep 2026 12:00:56 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/dynamic-typing-is-fun-until-it-isnt-lessons-from-python-and-c-176p</link>
      <guid>https://dev.to/pasindu_balasooriya/dynamic-typing-is-fun-until-it-isnt-lessons-from-python-and-c-176p</guid>
      <description>&lt;p&gt;As an undergraduate in Software Engineering, I began my programming journey with Python. Its basic simplicity, ease of use, flexibility and versatility made it easy for an amateur like me to understand the complex with less complexity. Python’s dynamic typing and concise syntax allowed me to write programmes quickly, experiment with different approaches and focus on solving problems. But when I started learning C#, I had to deal with a whole new level of static typing which completely changed my approach to coding. With that being said, having learnt python with a language-agnostic slant, the transition was not difficult; rather it was novel.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To learn more about my language-independent coding, read my article &lt;a href="https://medium.com/@pasindudilshanbalasooriya/the-cross-lingual-coder-python-practices-that-translate-everywhere-cfc3056db9dd" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To learn more about my language-independent coding, read my article here.&lt;/p&gt;

&lt;p&gt;In this article, I will explain how learning C# after Python made me appreciate the benefits of static typing, and how it improved my coding practices thus making my code reliable and easier to maintain.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2legzsr1fewl9wlbi7up.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2legzsr1fewl9wlbi7up.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both static typing and dynamic typing refer to how programming languages handle variable types. But they differ in when type checking occurs.&lt;/p&gt;

&lt;p&gt;In static typing the type of any given variable is known at compile-time (this refers to the phase when the source code is translated into the bytecode). That means you must define the type of each variable when you declare it. Should there be a type mismatch the compiler catches the error before the programme is executed and throws a compile-time error.&lt;/p&gt;

&lt;p&gt;In contrast, Python is dynamically typed, meaning that the type of variable is determined at runtime (refers to when the bytecode is being executed). Definition of variable type is not needed upon declaration as the interpreter checks the types as the code is executed.&lt;/p&gt;

&lt;p&gt;Let’s explore the differences using a few code examples.&lt;/p&gt;

&lt;p&gt;Python — Dynamic typing&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello world!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; 
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python, being very flexible, has no constraints on variable types. As in the above example, you can assign an integer value to a variable and later change it to a string without any sort of error being returned. While this is great for quality prototyping, this can also lead to unexpected bugs during runtime.&lt;/p&gt;

&lt;p&gt;C# — Static typing&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Hello, world!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Error&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In C#, type mismatch is immediately identified by the compiler and an error is thrown, should you attempt to assign an integer to a string variable. This prevents runtime type errors making your code more predictable and cleaner.&lt;/p&gt;

&lt;p&gt;Both Python and C# provide structures to capture multiple values with varying implementation strategies and type checking.&lt;/p&gt;

&lt;p&gt;Python Lists (dynamic)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;my_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.14&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Python, you can store different types of data in the same list without issues. While this increases flexibility, you are at a disadvantage because you do not know the types of elements ahead of time.&lt;/p&gt;

&lt;p&gt;C# Arrays (static)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;myArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, myArray is an integer array and you cannot assign a string to one of its elements. This guarantees that the array will only contain integers, and any operations done on it will be type-safe (meaning that the compiler ensures operations are performed only on compatible types).&lt;/p&gt;

&lt;p&gt;Dynamic typing in classes using Python&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; makes a sound.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;dog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python allows you to create classes without explicitly defining types for properties, making it easier to work with but harder to detect errors.&lt;/p&gt;

&lt;p&gt;Static typing in classes using C#&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Animal&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Animal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Speak&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; makes a sound."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;Animal&lt;/span&gt; &lt;span class="n"&gt;dog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Animal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Dog"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;dog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Speak&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In C#, properties like Name have clearly defined types (string), and the compiler will enforce these types throughout the code. This makes the code more robust (meaning that the code is strong, resilient and able to handle unexpected inputs without crashing) and less prone to runtime errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) Early error detection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Static typing helps you catch errors early in the development process and with languages like C#, the compiler ensures that you are working with correct types, preventing many common mistakes that might only surface at runtime in dynamically typed languages like Python.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;PrintSum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;PrintSum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"20"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As shown above, the C# compiler will immediately flag the error because you are trying to pass a string to a method that expects an integer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Code clarity and readability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With static typing, the data types are explicit, which makes the code more predictable and easier to understand. When you can see the clearly defined variable of type int or string, you know exactly what kind of data you are working with.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;greeting&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Hello, world!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In contrast, Python does not provide this calibre of clarity, where data types are implicitly handled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;greeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, world!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# No explicit type declaration
&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3) Improved refactoring and IDE support&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Static typing allows your Intergrated Development Environment (IDE) to provide better support for refactoring (the process of restructuring code without changing the functionality for better readability), and auto-completion tools. Because these types are defined at compile time, IDEs can predict the methods and properties you will need, leading to accurate and accelerated code changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;why static typing makes refactoring easier&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the main advantages of static typing is how it simplifies refactoring. In Python, you can easily change variable names, data types, and function signatures. But the risk is that you might break something unknowingly as there is no compile-time checking.&lt;/p&gt;

&lt;p&gt;In C#, the static typing system ensures that the compiler will let you know if you change a function’s signature and a consequent type mismatch occurs. This comes in handy when large codebases are concerned where making changes could affect multiple parts of the project.&lt;/p&gt;

&lt;p&gt;In the end, learning C# after Python helped me understand how valuable static typing can be. It made my code more predictable, easier to understand, reduced silly mistakes, and taught me to think more carefully about how to implement a solution with robust code. While Python is great for getting started quickly and obviously many other things, learning a statically typed language taught me structure and precision. With that being said, knowing both inarguably made me a better, well-rounded programmer.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Operator’s Identity Crisis: Polymorphism of the plus (+) Operator</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Tue, 08 Sep 2026 11:54:17 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/the-operators-identity-crisis-polymorphism-of-the-plus-operator-51l8</link>
      <guid>https://dev.to/pasindu_balasooriya/the-operators-identity-crisis-polymorphism-of-the-plus-operator-51l8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fea4eogcc3uc1ss8ivadi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fea4eogcc3uc1ss8ivadi.png" alt=" " width="720" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few months ago, as a beginner in programming, it always made me wonder how the + operator behaves with such functional versatility across various use cases in Python, which is my first programming language. Having deliberated this dynamic, a few clicks on google made me realise that this concept exists and that it’s called a polymorphic behaviour. In this article, I will walk you through a library of codes that I came across later when I started working with different programming languages which solidified my understanding of this theory.&lt;/p&gt;

&lt;p&gt;Polymorphism is a significant concept in object-oriented programming which enables a single and otherwise exclusive function to behave differently based on context. While polymorphism is often coupled with functions, operators such as the plus (+) operator exhibit this behavior just as well. This article explores how the plus operator demonstrates polymorphism across various programming languages. From numeric addition of the unary + to string concatenation, the + operator shows different masks depending on the data type being interacted with.&lt;/p&gt;

&lt;p&gt;Simply put, polymorphism accommodates a single interface to represent different underlying formats or forms. In programming, polymorphism is generally associated with functions or operators when and where their performance and by extension, behavior changes according to the data type. The most typical and commonplace example is the instance of how a method behaves differently when it is called depending on the class of the object on which it is called. Beyond this, polymorphism also applies to operators.&lt;/p&gt;

&lt;p&gt;In languages that support operator overloading(This refers to the ability to redefine or customise the behaviour of operators like +,-,*,/, with user defined data types like classes. Programming languages like C++ allows operator overloading. While java does not natively support this, the sole exception is the + operator.) ,or implicit type coercion (the automatic conversion of a value from one data type to another, without explicit manipulation from the programmer), the + operator can perform a varied list of tasks. Understanding the polymorphism of the + operator is important to mastering the subtleties of different programming languages and concepts.&lt;/p&gt;

&lt;p&gt;The plus operator serves multiple roles depending on the context in which they are being used. Now let’s have a look at some code snippets that I happened across during studies, where you can compare and contrast to get a concrete understanding.&lt;/p&gt;

&lt;p&gt;The + operator is used most commonly to perform arithmetic addition which is the standard behaviour in all the programming languages I have worked with so far.&lt;/p&gt;

&lt;p&gt;Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;  &lt;span class="c1"&gt;# Output: 8
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Java:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Output: 8&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Output: 8&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// result is 8&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In many programming languages, the + plus operator also functions as the key binding element in string concatenation.&lt;/p&gt;

&lt;p&gt;JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;greeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;World!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Output: "Hello, World!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Java:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;greeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Hello, "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"World!"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greeting&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Output: "Hello, World!"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Program&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;greeting&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Hello, "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"World!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Output: "Hello, World!"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;greeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Output: "Hello, World!"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In some languages like Python, the + operator can also be used to concatenate(join) lists or arrays instead of the extend() method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;list1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;list2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;combined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;list1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;list2&lt;/span&gt;  &lt;span class="c1"&gt;# Output: [1, 2, 3, 4, 5, 6]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Java, the language not having accommodated operator overloading, attempting to do this would result in a compiler error. C# and JavaScript do not allow this action to be performed on their respective platforms as well.&lt;/p&gt;

&lt;p&gt;Type coercion or type casting refers to the process of converting a value from one data type to another.&lt;/p&gt;

&lt;p&gt;JavaScript is famous for its implicit type coercion. When the + operator is used with a number and a string, it converts the number to a string and concatenates them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Score: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Output: "Score: 10"&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;anotherResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;anotherResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Output: "51"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Java also performs automatic type coercion when a String is one of the operands. The other operand (e.g., an int) is converted to its string representation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Agent "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Output: "Agent 7"&lt;/span&gt;

        &lt;span class="c1"&gt;// The integer 42 is coerced into the string "42"&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;anotherResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" is the answer."&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anotherResult&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Output: "42 is the answer."&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;C# behaves very similarly to Java. The + operator triggers string concatenation if at least one operand is a string, converting the other types.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Program&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Level "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;99&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Output: "Level 99"&lt;/span&gt;

        &lt;span class="c1"&gt;// The integer 3 is coerced into the string "3"&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;anotherResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" lives remaining"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anotherResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Output: "3 lives remaining"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python is more strict. It does not perform automatic type coercion between strings and numbers with the + operator. Trying to do so will raise a TypeError. You must explicitly convert the types when and where necessary.&lt;/p&gt;

&lt;p&gt;While polymorphism of the + operator can help simplify coding, it can also lead to confusion and errors if not properly understood in a platform dependent way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmasbtq2exz3d4xx1964h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmasbtq2exz3d4xx1964h.png" alt=" " width="720" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of the main challenges is implicit type casting in JavaScript which can lead to unexpected results when numbers and strings are combined, or when non-compatible data types are used.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In JavaScript, adding null or undefined values to a string can lead to unexpected behaviour.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Python will raise a TypeError should you attempt to concatenate incompatible types. (eg: A string and a number).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;*Performance overhead (the additional resources such as time and memory that a process may consume beyond what is necessary to achieve a set goal) in JavaScript can arise due to type coercion ,especially when large datasets are considered.&lt;/p&gt;

&lt;p&gt;In order to avoid confusion and ensure smooth usage of the + operator, I recommend following these best practices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Be explicit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In dynamically typed languages like JavaScript, always make sure that your operands are of the same type before using + operator.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;2. Use type checking&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
In languages like Python, consider performing type checks using functions such as type() before using the + operator to avoid runtime errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Know your programming language&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Understand how + operator works in the language that you are working with, by referring to and reviewing documentation to see how it handles data types and coercion.&lt;/p&gt;

&lt;p&gt;The polymorphism of the + operator highlights the flexibility and the complexity inherent in many a programming language. Streching from numeric addition to string concatenation and more complex operations like list merging, the polymorphism of the + can be both helpful and challenging simultaneously. By understanding the behavior dynamics of the operator, we can write cleaner and more predictable code while avoiding pitfalls like type coercion. Irrespective of the programming language you are working with, understanding operator polymorphism is essential to master nuanced manipulation of data types.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>Containerization: A More Streamlined Approach to Deployment</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Tue, 08 Sep 2026 11:48:59 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/containerization-a-more-streamlined-approach-to-deployment-4f55</link>
      <guid>https://dev.to/pasindu_balasooriya/containerization-a-more-streamlined-approach-to-deployment-4f55</guid>
      <description>&lt;p&gt;As a beginner in Software Engineering, I understand quite well for my own good, how complex deploying an application can turn out to be, especially when resource management and performance optimization is concerned. Before containers came into play (and no, BC does not mean ‘before containerization’) developers relied mainly on VMs which is short for Virtual Machines, to deploy applications. While VMs were a solid way to create isolated environments that accommodated applications to be run independently from the OS of the host machine, they also came with their own set of challenges, when it comes to effectiveness. Now let me elaborate on this last line a little bit more.&lt;/p&gt;

&lt;p&gt;Virtual machines, once being the go-to solution for application deployment in isolated environments, had their own set of drawbacks even though they provided an entire OS for each application to simulate a server environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.Heavy Resource Consumption&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each application deployment requires allocation of resources for an entire OS as each VM requires a full operating system to run and this results in excessive resource usage with the VM consuming memory and CPU power which is as resource-intensive and no less inefficient as building a new city every time you want to add a new house.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd15sdig3i3zv9cfcs8lf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd15sdig3i3zv9cfcs8lf.png" alt=" " width="720" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.Slow Deployment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since VMs entail booting up the entire OS, starting up an application is time-consuming and considerably slow meaning that VMs are not simply fast enough should you need to deploy and scale applications quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Environment Inconsistency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While VMs are isolated, they can still differ from one another in terms of configuration, giving rise to the famous or rather the infamous “It works on my machine” problem. For instance, an application may work in your local VM seamlessly but fail to do so when deployed in production due to subtle configuration differences.&lt;/p&gt;

&lt;p&gt;The above drawbacks paved the way for the emergence of containerization. Containers, dissimilar to VMs, do not require their own operating system. Instead, they package the application with everything it needs to run such as packages, binaries and configurations into a lightweight, isolated environment.&lt;/p&gt;

&lt;p&gt;Containerization is hot right now 💅 and here’s why&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Resource Efficiency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Containers eliminate the need for their own OS for each instance by sharing the kernel of the host OS making them much lighter and resource efficient. A practical analogy to this would be using a cost-effective bicycle for a short trip instead of a gas-guzzling car.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Faster Deployment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Starting up almost instantly, containers remove the need to boot the full&lt;/p&gt;

&lt;p&gt;OS giving them enhanced productivity, much similar to the quick and efficient act of flipping a light switch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Consistency Across Environments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Containers ensure that your application will run the same across all development, testing, and production environments, eliminating the “It works on my machine” problem we talked about before. Rest assured, if it works in the container in your laptop, you won’t hear your colleague complain (about the container, that is, at any rate)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3s74g1hkamdd4p00t84t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3s74g1hkamdd4p00t84t.png" alt=" " width="720" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Real-life Example&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine you are managing an E-commerce platform deployed on a VM which is gaining popularity. During the holidays, traffic spikes significantly and the VM struggles as demand increases. As VMs consume a large number of resources, they cause slowdowns and resource allocation issues and decelerates scaling. On top of all that, the “It works on my machine” problem arises due to environment inconsistency.&lt;/p&gt;

&lt;p&gt;However, if your platform used Docker containers managed by Kubernetes, scaling would be much faster and more efficient. Containers being lightweight allows you to run more instances with lesser resource consumption. Kubernetes would automate scaling to optimize peak hour performance. Additionally, Docker harmonizes consistency across all environments removing the configuration dissimilarities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docker is a platform that simplifies the process of creating, deploying, and running containers, allowing developers to package the application along with its dependencies, into a single container.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Docker Image- This is the blueprint for creating containers which constitutes all the necessary configurations and dependencies needed to run the application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker Container- This is the actual running instance of the application created from the Docker Image.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes is the tool that manages the containers, orchestrating everything from scaling up containers to recovery(self-healing). (If Docker is the builder, Kubernetes is the project manager). This automates the deployment, scaling and operation of containerized applications and ensures effective resource allocation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl07rnn6b3gzu7bwv0ge2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl07rnn6b3gzu7bwv0ge2.png" alt=" " width="720" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s why containerization became a game-changer for modern application development.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Portability&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Containers can run on any machine that supports Docker, be it a local machine, physical server, or in the cloud, and you can deploy it from anywhere, much like packing your things into a suitcase.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Efficiency&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;More resource-efficient (less time-consuming and more cost-effective) because containers share the kernel of the host OS.&lt;/p&gt;

&lt;p&gt;3.Scalability&lt;/p&gt;

&lt;p&gt;Containers are easy to scale up and down. Kubernetes can add more automation to handle peak performance.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Faster Development Cycle&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Testing, deployment and iteration, made easy by containerization, makes it easy to collaborate in teams and work on parallelly, leading to faster releases and fewer bugs.&lt;/p&gt;

&lt;p&gt;Adopting containerization was a revolutionary shift that modernized how we manage and deploy applications. Along with Docker and Kubernetes, containerization offers a lighter, faster way to run applications across environments. Much like moving from a traditional office building to a modular co-working space, you can work more efficiently, save resources and work faster. Containerization, supported by Docker and Kubernetes, is a modern solution that makes the deployment process more scalable, reliable, and portable.&lt;/p&gt;

</description>
      <category>deployment</category>
      <category>devops</category>
      <category>docker</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Inside WSO2-ThunderID’s Post-Quantum JWKS</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Tue, 25 Aug 2026 18:13:10 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/inside-wso2-thunderids-post-quantum-jwks-3433</link>
      <guid>https://dev.to/pasindu_balasooriya/inside-wso2-thunderids-post-quantum-jwks-3433</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgkb29si28fam34c2oh7t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgkb29si28fam34c2oh7t.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everyone worried about the wrong thing.&lt;/p&gt;

&lt;p&gt;When people talk about migrating to post-quantum cryptography, the first question is almost always performance. Will it slow down my auth server? Will verification cost me latency on every request?&lt;/p&gt;

&lt;p&gt;I spent a while measuring this inside ThunderID, an open source IAM stack that ships ML-DSA signing today and the performance answer turns out to be boring. Signing a token with ML-DSA is faster than RSA-2048. Verification is in the same ballpark as ECDSA.&lt;/p&gt;

&lt;p&gt;The problem is size. And size breaks things that speed never would.&lt;/p&gt;

&lt;h3&gt;
  
  
  First, here is one
&lt;/h3&gt;

&lt;p&gt;This is a real JWKS document, produced by ThunderID’s JWKS service with an ML-DSA-87 signing key loaded. I have trimmed the public key in the middle because the full thing is 3,456 characters of base64 and would eat half this article.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"q_Gzy1Asrf8PhpN7ZFg2pR8_HADURDcvfEZOwqDHYXI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"kty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AKP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"use"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sig"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ML-DSA-87"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"pub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cxfw7KPEFQQyjHaDETwOayeFNC09h1tYRLxwkyvt4q_xV0WoqsH3OK...
              ...5GfdFtksLtA49sTwfiDp0iAZCplRqumBlc72"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at kty. Not RSA, not EC, not OKP. &lt;strong&gt;AKP&lt;/strong&gt; , for Algorithm Key Pair is a JWK key type introduced for exactly this. If your client library has a switch statement over key types and it almost certainly does, that switch does not have a branch for this yet.&lt;/p&gt;

&lt;p&gt;And the token it signs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;header : {"alg":"ML-DSA-87","kid":"mldsa87","typ":"JWT"}
payload : {"aud":"my-app","exp":1790000000,"iss":"https://localhost:8090",
           "scope":"openid profile email","sub":"8f3a-user"}
length : 6386 bytes (signature alone: 4627 bytes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six and a half kilobytes for a token carrying five claims. The claims are 118 bytes of that. Everything else is signature.&lt;/p&gt;

&lt;h3&gt;
  
  
  The numbers
&lt;/h3&gt;

&lt;p&gt;Here is what each algorithm costs. I generated these through ThunderID’s own signing path rather than copying them from a spec, so they include the base64url encoding you actually pay for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frnf9dt6u323bwe88a1x2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frnf9dt6u323bwe88a1x2.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Going from ES256 to ML-DSA-87 takes a 302 byte token to roughly 6.4KB. That is about 21 times bigger. Against RS256 it is still more than 11 times.&lt;/p&gt;

&lt;p&gt;Timing, for contrast, was a non-event. RS256 signing sat around 630µs across runs. ML-DSA signing ranged from about 95µs to 520µs. Verification for everything landed between single digit and 60µs, close enough to noise at this resolution that I would not read anything into the ordering.&lt;/p&gt;

&lt;p&gt;That ML-DSA signing range is wide on purpose. ML-DSA uses rejection sampling, so it loops until it gets a signature meeting its constraints. Sometimes that is one pass, sometimes several. Benchmark it once and write down the number and you are quoting a coin flip. The sizes, by contrast, are fixed by the parameter set and did not move at all between runs.&lt;/p&gt;

&lt;p&gt;The CPU cost of post-quantum signatures has quietly stopped being the interesting problem. The bytes have not.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where 6KB tokens actually hurt
&lt;/h3&gt;

&lt;p&gt;A 6KB token is fine sitting in a database. It stops being fine the moment it has to travel somewhere with a limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cookies cap at 4KB.&lt;/strong&gt; This is the one that bites first. RFC 6265 only requires browsers to support 4096 bytes per cookie and that is what they do in practice. An ML-DSA-87 token does not fit in a cookie at all. Neither does ML-DSA-65 once you add a realistic claim set. If your session design puts a signed token in a cookie, that design does not survive the migration without splitting the token or moving to a session reference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proxies cap request headers.&lt;/strong&gt; nginx defaults large_client_header_buffers to 8KB and plenty of API gateways sit in the same range. One 6KB bearer token in an Authorization header usually squeaks through. That token plus normal headers or two tokens on one request will not. These failures are miserable to debug because they surface as a generic 400 from an intermediary that never tells you which header was too big.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;URLs cap hardest.&lt;/strong&gt; Anything putting a token in a query string or fragment is finished. The old advice about keeping URLs under 2,000 characters was already tight. A single post-quantum signature blows past it on its own.&lt;/p&gt;

&lt;p&gt;None of this is an argument against post-quantum. It is an argument for finding out now which parts of your architecture quietly assume a token fits somewhere because those assumptions stay invisible until they break.&lt;/p&gt;

&lt;h3&gt;
  
  
  The part where the standard library lets you down
&lt;/h3&gt;

&lt;p&gt;Here is the detail I did not expect.&lt;/p&gt;

&lt;p&gt;Go’s standard library has no ML-DSA support. Not in crypto, not in crypto/x509. So if you want to load an ML-DSA private key from a PKCS#8 file, which is how anyone normally ships a signing key, you parse the ASN.1 yourself.&lt;/p&gt;

&lt;p&gt;ThunderID has a file doing exactly that, and it is refreshingly honest about it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ML-DSA PKCS#8/ASN.1 key encoding helpers (RFC 9881). These fill the gap left&lt;/span&gt;
&lt;span class="c"&gt;// by the Go standard library's lack of ML-DSA support. When crypto/x509 gains&lt;/span&gt;
&lt;span class="c"&gt;// ML-DSA support (Go 1.27), delete this file and replace callers with&lt;/span&gt;
&lt;span class="c"&gt;// x509.ParsePKCS8PrivateKey / x509.MarshalPKCS8PrivateKey.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A whole file of hand-written ASN.1 with a note saying delete me when the language catches up. That is what post-quantum ready looks like in practice right now, and it is a long way from flipping a config value.&lt;/p&gt;

&lt;p&gt;It goes further. RFC 9881 lets an ML-DSA private key be encoded three different ways: as a seed, as an expanded key or as a SEQUENCE holding both. All three are valid, so the parser accepts all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ParseMLDSAPKCS8 parses a DER-encoded RFC 9881 PKCS#8 ML-DSA private key. It&lt;/span&gt;
&lt;span class="c"&gt;// accepts all three private-key CHOICE encodings (seed [0], expandedKey, and&lt;/span&gt;
&lt;span class="c"&gt;// the "both" SEQUENCE), preferring the seed when present.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are planning your own migration, that is the interop landmine worth knowing about. Whichever tool generates your key picks an encoding and whatever consumes it has to handle the one you got. “It’s a standard PKCS#8 file” is doing a lot of work in that sentence.&lt;/p&gt;

&lt;p&gt;The certificate side is stranger still. Go cannot parse an ML-DSA public key out of an X.509 certificate either, so the loader keeps the certificate DER as an opaque blob and derives the public key from the private key instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ML-DSA: the standard library cannot parse the certificate's public&lt;/span&gt;
&lt;span class="c"&gt;// key, so derive it from the configured private key.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything works. It just works around the language rather than with it and it will keep doing so until Go 1.27.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crypto-agility is not a config flag
&lt;/h3&gt;

&lt;p&gt;The other thing that becomes obvious in a real implementation is how many places the algorithm choice reaches.&lt;/p&gt;

&lt;p&gt;It is not just the signing call. It is the JWKS endpoint, which has to publish the right alg and kid so relying parties can verify. It is key management and storage. It is the discovery document advertising what the server supports. It is per-client negotiation, because in any realistic migration different clients are ready at different times and a client that cannot verify ML-DSA needs to keep getting ES256 without anyone running a flag day.&lt;/p&gt;

&lt;p&gt;ThunderID’s config models this as a list of keys with an explicit choice of which one signs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;crypto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-key"&lt;/span&gt;
      &lt;span class="na"&gt;cert_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config/certs/signing.cert"&lt;/span&gt;
      &lt;span class="na"&gt;key_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config/certs/signing.key"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mldsa87"&lt;/span&gt;
      &lt;span class="na"&gt;cert_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config/certs/mldsa87.cert"&lt;/span&gt;
      &lt;span class="na"&gt;key_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;config/certs/mldsa87.key"&lt;/span&gt;

&lt;span class="na"&gt;jwt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;preferred_key_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default-key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiple keys registered at once, one selected as preferred. That shape matters more than it looks. A migration is not a switch from old to new, it is a stretch of time where both are live, both are published in the JWKS and traffic moves over gradually. A design holding only one signing key at a time cannot express that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trying it yourself
&lt;/h3&gt;

&lt;p&gt;You need OpenSSL 3.5 or later to generate an ML-DSA key, which is the main friction right now. Most distributions are still on 3.0 to 3.4 and ML-DSA landed in 3.5.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl genpkey &lt;span class="nt"&gt;-algorithm&lt;/span&gt; ML-DSA-87 &lt;span class="nt"&gt;-out&lt;/span&gt; mldsa87.key
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; mldsa87.key &lt;span class="nt"&gt;-out&lt;/span&gt; mldsa87.cert &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s2"&gt;"/CN=thunderid-signing"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point a key entry at those two files, restart and the JWKS above is what you get.&lt;/p&gt;

&lt;p&gt;If your OpenSSL is older, the ThunderID repo carries generated ML-DSA fixtures for all three parameter sets under backend/internal/system/kmprovider/defaultkm/pki/testdata/, which is what I used for the output in this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should you actually do this
&lt;/h3&gt;

&lt;p&gt;Not yet, for most people. The reason to care now is that the threat model was never about today.&lt;/p&gt;

&lt;p&gt;Harvest-now-decrypt-later means someone records your encrypted traffic today and decrypts it when the hardware arrives. The signature equivalent is trust-now-forge-later, an assertion you sign today with a long validity becomes forgeable the moment the underlying assumption falls. If you issue credentials meant to be trusted for years the clock on those started when you issued them, not when quantum computers show up.&lt;/p&gt;

&lt;p&gt;So the useful question is not “should I switch today” but “how long are my signatures meant to be trusted and do I know what breaks when I switch”. The second half you can answer this afternoon. Take your longest realistic token, add 6KB and go look at every place it travels.&lt;/p&gt;

&lt;p&gt;The measuring is cheap. The architectural assumptions you find are the expensive part and they do not get cheaper by waiting.&lt;/p&gt;

</description>
      <category>ami</category>
      <category>thunderid</category>
      <category>postquantumcryptogra</category>
      <category>wso2</category>
    </item>
    <item>
      <title>Markdown is the quietly powerful language you already almost know</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Wed, 12 Aug 2026 15:01:54 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/markdown-is-the-quietly-powerful-language-you-already-almost-know-416c</link>
      <guid>https://dev.to/pasindu_balasooriya/markdown-is-the-quietly-powerful-language-you-already-almost-know-416c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fds3d9whyvk7fnhk94692.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fds3d9whyvk7fnhk94692.jpg" width="799" height="480"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://serokell.io/blog/markdown-editor-tips" rel="noopener noreferrer"&gt;https://serokell.io/blog/markdown-editor-tips&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You have probably written Markdown without realizing it. That asterisk you typed around a word in a Slack message to make it bold? Markdown. The hash symbol you threw before a heading in a README? Also Markdown. It is one of those rare tools that feels natural before you even learn it properly.&lt;/p&gt;

&lt;p&gt;This article is a complete guide to Markdown with what it is, why it matters and how to use it with real examples you can see rendered live.&lt;/p&gt;
&lt;h3&gt;
  
  
  What is Markdown?
&lt;/h3&gt;

&lt;p&gt;Markdown is a lightweight markup language created by &lt;strong&gt;John Gruber&lt;/strong&gt; in 2004. The idea was simple. Write plain text that reads naturally and let a program convert it to formatted HTML. Instead of wrestling with &lt;/p&gt;
&lt;h1&gt; tags and &lt;strong&gt; wrappers, you write human-readable symbols that carry their own meaning.&lt;br&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Hello, World

This is a **very** important message.
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq1p3pq6zvmyy9e6yl3bt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq1p3pq6zvmyy9e6yl3bt.png" width="670" height="125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same output. Far less noise.&lt;/p&gt;
&lt;h3&gt;
  
  
  Where Markdown is used
&lt;/h3&gt;

&lt;p&gt;Markdown is everywhere once you start looking. GitHub uses it for README files, issues and pull requests. Medium supports Markdown imports and keyboard shortcuts that mirror it. Notion, Obsidian and Confluence all render Markdown natively. Reddit uses a Markdown dialect for post formatting. Static site generators like Jekyll and Hugo build entire websites from Markdown files.&lt;/p&gt;

&lt;p&gt;Learning Markdown is a one-time investment that pays dividends across almost every platform a developer or writer touches.&lt;/p&gt;
&lt;h3&gt;
  
  
  The core syntax
&lt;/h3&gt;
&lt;h3&gt;
  
  
  Headings
&lt;/h3&gt;

&lt;p&gt;Use the # symbol to create headings. The number of # symbols corresponds to the heading level, from H1 to H6.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Heading 1
## Heading 2
### Heading 3
#### Heading 4
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftwz5rcq84ptexxan9k5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftwz5rcq84ptexxan9k5t.png" width="668" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Bold and italic
&lt;/h3&gt;

&lt;p&gt;Wrap text in **double asterisks** for bold and *single asterisks* for italic. You can combine them.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This is **bold text**.
This is *italic text*.
This is ***bold and italic*** at the same time.
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10wro6dcizrd04qgl2p1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10wro6dcizrd04qgl2p1.png" width="668" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Lists
&lt;/h3&gt;

&lt;p&gt;Unordered lists use -, * or + as bullet points. Ordered lists use numbers. You can nest lists by indenting with two or four spaces.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Unordered
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Milk
- Eggs
- Bread
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzsbguha9jlsi23kg9r2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyzsbguha9jlsi23kg9r2.png" width="666" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ordered
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Clone the repository
2. Install dependencies
3. Run the dev server
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fescxd9sqh40swgb1twzk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fescxd9sqh40swgb1twzk.png" width="666" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Nested
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- Frontend
  - React
  - Tailwind CSS
- Backend
  - Node.js
  - PostgreSQ
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbxal0t3u19e2mho3us11.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbxal0t3u19e2mho3us11.png" width="669" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Links and images
&lt;/h3&gt;

&lt;p&gt;Links follow the pattern &lt;a href="https://dev.toURL"&gt;link text&lt;/a&gt;. Images work the same way with an exclamation mark prepended. The text in square brackets becomes the alt text, which matters for accessibility.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read the [Markdown Guide](https://www.markdownguide.org) for more.
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1h894t1qgsa6aw71hwxt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1h894t1qgsa6aw71hwxt.png" width="663" height="107"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Code
&lt;/h3&gt;

&lt;p&gt;For inline code, wrap it in backticks. For code blocks, use triple backticks and optionally specify the language for syntax highlighting.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inline code
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use the `console.log()` function to debug.
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4dnyp78h11yo6t5d308.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4dnyp78h11yo6t5d308.png" width="665" height="87"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Code block
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;br&gt;
javascript&lt;br&gt;
function greet(name) {&lt;br&gt;
  return &lt;code&gt;Hello, ${name}!&lt;/code&gt;;&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;br&gt;
plaintext&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5p7x3nred8fzoqn1iani.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5p7x3nred8fzoqn1iani.png" width="662" height="138"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Tables
&lt;/h3&gt;

&lt;p&gt;Tables use pipes | and hyphens - to define structure. The second row separates the header from the body. You can align columns by adding colons to the separator row.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Basic table
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Name | Role | Status |
|------------|--------------|----------|
| Alice | Developer | Active |
| Bob | Designer | Active |
| Carol | QA Engineer | On leave |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;
plaintext&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft18da02m66dvvq13k3vc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft18da02m66dvvq13k3vc.png" width="666" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;With alignment
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| Left | Center | Right |
|:-----------|:------------:|---------:|
| aligned | aligned | aligned |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;br&gt;
plaintext&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9ud8kg2hds6u68bsa9g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9ud8kg2hds6u68bsa9g.png" width="665" height="127"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Task lists
&lt;/h3&gt;

&lt;p&gt;GitHub Flavored Markdown adds interactive checkboxes using - [x] for checked and - [] for unchecked items.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- [x] Set up the project
- [x] Write the README
- [] Add unit tests
- [] Deploy to production
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;br&gt;
plaintext&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg3rxghsblo3bxsw0mnbn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg3rxghsblo3bxsw0mnbn.png" width="665" height="162"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  A common mistake: escaping special characters
&lt;/h3&gt;

&lt;p&gt;What if you actually want to display an asterisk without triggering bold formatting? Use a backslash to escape it. The same trick works for #, [,], (, ) and other Markdown-reserved characters.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This is \*not bold\*.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;br&gt;
plaintext&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8k6hfatb44ypu2zp6vp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8k6hfatb44ypu2zp6vp.png" width="656" height="82"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  A real-world example: a project README
&lt;/h3&gt;

&lt;p&gt;Here is what a solid Markdown README looks like in practice. This is entirely plain text. No Word document, no HTML, no design tool. Just a .md file that renders beautifully on GitHub.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# MyApp

A lightweight REST API for managing personal tasks.

## Features

- Create, update, and delete tasks
- Assign due dates and priority levels
- Filter tasks by status

## Getting Started

### Prerequisites

- Node.js 18+
- PostgreSQL 14+

### Installation

1. Clone the repository:
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;br&gt;
bash&lt;br&gt;
   git clone &lt;a href="https://github.com/yourname/myapp.git" rel="noopener noreferrer"&gt;https://github.com/yourname/myapp.git&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2. Install dependencies: `npm install`
3. Create a `.env` file based on `.env.example`.
4. Start the server: `npm run dev`

## License

MIT
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7ys61aaea4hvjo334m4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7ys61aaea4hvjo334m4.png" width="562" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Markdown beats rich text editors
&lt;/h3&gt;

&lt;p&gt;Rich text editors hide their formatting inside binary or XML structures you cannot read or version-control easily. Markdown files are plain text, which means:&lt;/p&gt;

&lt;p&gt;Version control works perfectly.&lt;/p&gt;

&lt;p&gt;Git can diff a .md file line by line and show exactly what changed. Try doing that with a .docx.&lt;/p&gt;

&lt;p&gt;They are portable.&lt;/p&gt;

&lt;p&gt;A Markdown file opened in 2040 will look exactly the same as it does today. File formats rot. Plain text does not.&lt;/p&gt;

&lt;p&gt;They are distraction-free.&lt;/p&gt;

&lt;p&gt;You are not hunting through toolbar menus. The formatting lives in the text itself, which keeps you focused on writing.&lt;/p&gt;

&lt;p&gt;They convert to almost anything.&lt;/p&gt;

&lt;p&gt;With tools like Pandoc, a single Markdown file can become a PDF, an HTML page, a Word document or an ePub.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting started today
&lt;/h3&gt;

&lt;p&gt;You do not need to install anything. Open &lt;a href="https://stackedit.io/" rel="noopener noreferrer"&gt;StackEdit&lt;/a&gt; or &lt;a href="https://dillinger.io/" rel="noopener noreferrer"&gt;Dillinger&lt;/a&gt; in your browser. Type some Markdown on the left and watch it render on the right in real time.&lt;/p&gt;

&lt;p&gt;If you use VS Code, the built-in Markdown preview (Ctrl+Shift+V) is excellent. If you want a dedicated writing environment, Obsidian and Typora are both popular choices.&lt;/p&gt;

&lt;p&gt;The learning curve is genuinely shallow. Most people feel comfortable with the core syntax within an afternoon. What you get in return is a formatting language that works everywhere, lasts forever and never gets in your way.&lt;/p&gt;

&lt;p&gt;Start with a README for your next project. That is all it takes.&lt;/p&gt;

&lt;/strong&gt;
&lt;/h1&gt;

</description>
      <category>markdown</category>
      <category>md</category>
    </item>
    <item>
      <title>Agile, Scrum and Waterfall: Which One Actually Works?</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Tue, 07 Jul 2026 17:25:47 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/agile-scrum-and-waterfall-which-one-actually-works-18p0</link>
      <guid>https://dev.to/pasindu_balasooriya/agile-scrum-and-waterfall-which-one-actually-works-18p0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F1024%2F0%2A3kNwuZiRhOrbxSTd" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F1024%2F0%2A3kNwuZiRhOrbxSTd" width="1024" height="576"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://www.easy8.com/EasyRedmine/media/waterfall-vs-agile.png?width=1920&amp;amp;height=0&amp;amp;rmode=min&amp;amp;quality=75&amp;amp;token=a7vd96XfaUO%2Bniyr13y7Er0VdEJpUGpsapr6kxMghEo%3D" rel="noopener noreferrer"&gt;https://www.easy8.com/EasyRedmine/media/waterfall-vs-agile.png?width=1920&amp;amp;height=0&amp;amp;rmode=min&amp;amp;quality=75&amp;amp;token=a7vd96XfaUO%2Bniyr13y7Er0VdEJpUGpsapr6kxMghEo%3D&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you have ever worked on a project, whether it was building software, planning an event or even renovating a house, you have probably run into one big question. Do you plan everything out from start to finish before you begin, or do you build a little, check in, adjust and keep going?&lt;/p&gt;

&lt;p&gt;That question is really the heart of the whole Agile versus Waterfall debate. And once you understand it, a lot of the buzzwords thrown around in offices (sprints, backlogs, scrum masters) start to make a lot more sense.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Is the SDLC?
&lt;/h3&gt;

&lt;p&gt;Before getting into Agile versus Waterfall, it helps to know about the SDLC, short for Software Development Life Cycle. This is just the general set of stages any piece of software tends to pass through on its way from idea to finished product. Most versions of it include the same rough stages such as figuring out requirements, designing a solution, building it, testing it, then releasing and maintaining it.&lt;/p&gt;

&lt;p&gt;The SDLC itself is not a methodology. It is more like the skeleton. Waterfall and Agile are two very different ways of moving through that same skeleton. Waterfall walks through those stages once, in a straight line, start to finish. Agile walks through a small version of those same stages again and again, once per sprint, refining as it goes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F187bsm3f8n392rfife32.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F187bsm3f8n392rfife32.png" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Old Way: Waterfall
&lt;/h3&gt;

&lt;p&gt;Waterfall is the traditional approach to managing a project. The name comes from how it works. Like water flowing down a series of steps, you move through one phase completely before falling into the next one. There is no going back up.&lt;/p&gt;

&lt;p&gt;A typical Waterfall project looks like this.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Requirements - figure out exactly what needs to be built&lt;/li&gt;
&lt;li&gt;Design - plan out how it will be built&lt;/li&gt;
&lt;li&gt;Implementation - actually build it&lt;/li&gt;
&lt;li&gt;Testing - check that it works&lt;/li&gt;
&lt;li&gt;Release and maintenance - launch it and keep it running&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The appeal of Waterfall is obvious. It is simple, predictable and easy to explain to a client or manager. You agree on requirements upfront by signing an SRS (software requirement specification), you get a timeline, and everyone knows what is coming next. For decades this was the default way software and other big projects got built.&lt;/p&gt;

&lt;p&gt;The problem is real life rarely stays still long enough for this to work perfectly. If a client changes their mind halfway through or the market shifts or testing reveals a huge design flaw near the end, going back to fix it can be expensive and slow. By the time you find out something is wrong, you may have already spent months building on top of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Newer Way: Agile
&lt;/h3&gt;

&lt;p&gt;Agile grew out of frustration with exactly that problem. Instead of planning the entire project upfront and hoping nothing changes, Agile assumes change is normal and builds a process around it.&lt;/p&gt;

&lt;p&gt;Rather than one long march from requirements to release, Agile breaks the work into small, workable chunks. Each chunk delivers something usable (or shippable), even if it is small and the team gets feedback quickly instead of waiting until the very end to find out if they got it right.&lt;/p&gt;

&lt;p&gt;Agile is built around a few core values.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Individuals and interactions over rigid processes and tools&lt;/li&gt;
&lt;li&gt;Working products over exhaustive documentation&lt;/li&gt;
&lt;li&gt;Customer collaboration over locking in a fixed contract upfront&lt;/li&gt;
&lt;li&gt;Responding to change over blindly following a plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F77aflcqkno8fve5yes14.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F77aflcqkno8fve5yes14.png" width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Scrum: Agile in Practice
&lt;/h3&gt;

&lt;p&gt;Agile itself is more of a mindset than a strict process, which is why frameworks like Scrum exist. Scrum takes the Agile philosophy and turns it into an actual set of practices a team can follow day to day.&lt;/p&gt;

&lt;p&gt;Here is roughly how it works. The team breaks the project into short cycles called sprints, usually somewhere between one and four weeks long. Before a sprint starts, the team picks items from a running wishlist called the product backlog and commits to finishing them during that sprint (sprint backlog).&lt;/p&gt;

&lt;p&gt;A few roles keep things moving.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Product Owner decides what matters most and keeps the backlog in order&lt;/li&gt;
&lt;li&gt;The Scrum Master helps the team stay on track and clears away anything blocking progress&lt;/li&gt;
&lt;li&gt;The Scrum Team actually builds the thing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During the sprint, the team holds a quick daily standup, usually capped at around 15 minutes, where everyone answers three simple questions like what did I do yesterday, what am I doing today and is anything blocking me. It sounds small, but it keeps everyone aligned without turning into a three hour meeting.&lt;/p&gt;

&lt;p&gt;At the end of the sprint, the team demos what they built in a sprint review and then holds a retrospective to talk honestly about what went well and what did not. Then the whole cycle starts again with the next sprint.&lt;/p&gt;

&lt;p&gt;One helpful tool inside this process is the user story. Instead of writing a dense technical requirement, teams write something like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;As a [type of user], I want [some goal], so that [some reason].&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each user story also comes with acceptance criteria, which are the specific conditions that must be true for the story to count as done. For example, a signup form user story might require that a form cannot be submitted with missing fields, that submitted data gets stored correctly and that a confirmation email goes out afterward. This keeps everyone honest about what “finished” actually means.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn2unjq2muwa1enmi91mz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn2unjq2muwa1enmi91mz.png" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  So Which One Is Actually Better?
&lt;/h3&gt;

&lt;p&gt;Here is the honest answer. It depends, but Agile wins more often than not in today’s world.&lt;/p&gt;

&lt;p&gt;Agile tends to work better when requirements are likely to change, when you need to get something in front of users early or when the project is complex enough that nobody can predict every detail upfront. This describes most modern software projects. Customers change their minds, markets shift and the fastest way to learn if an idea works is to build a small version and test it, rather than spend a year building the “perfect” plan only to find out it missed the mark.&lt;/p&gt;

&lt;p&gt;That said, Waterfall still has its place. If you are working on something with fixed, well understood requirements that are extremely unlikely to change, like certain construction projects, regulatory compliance work or hardware manufacturing where redoing a physical step is costly or impossible, a predictable step by step plan can actually be safer and more efficient. Waterfall also tends to suit situations where the client wants a fixed price, fixed scope contract agreed upfront, since Agile’s flexible scope can be harder to pin down in a contract.&lt;/p&gt;

&lt;p&gt;So the real answer is not “Agile is always right.” It is closer to this. The more uncertain, fast changing and feedback dependent your project is, the more Agile and Scrum make sense. The more fixed, predictable and sequential your project is, the more Waterfall holds up.&lt;/p&gt;

&lt;p&gt;Most teams today lean Agile because most modern work, especially software, lives in that first category. But a good project manager does not pick a methodology out of habit. They look at the actual project in front of them and choose the approach that fits.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Takeaway
&lt;/h3&gt;

&lt;p&gt;Waterfall gives you structure and predictability by planning everything before you start. Agile and Scrum as its most common framework, gives you flexibility and fast feedback by building in small pieces and adjusting as you go. Neither one is universally correct. The best project managers know both well enough to pick the right tool for the job and sometimes even blend the two depending on what a project actually needs.&lt;/p&gt;

</description>
      <category>designsprint</category>
      <category>scrum</category>
      <category>agilemethodology</category>
      <category>projectmanagement</category>
    </item>
    <item>
      <title>What Building an Audit Trail Taught Me About WSO2 Identity Server</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Sun, 05 Jul 2026 11:55:37 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/what-building-an-audit-trail-taught-me-about-wso2-identity-server-21p8</link>
      <guid>https://dev.to/pasindu_balasooriya/what-building-an-audit-trail-taught-me-about-wso2-identity-server-21p8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3shvnf0y1kn8jrykj7mc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3shvnf0y1kn8jrykj7mc.png" width="799" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I spent the last few weeks building &lt;strong&gt;AuditTrail&lt;/strong&gt; , a small but deliberately serious payment-gateway compliance system, mostly as a way to get real hands-on experience with WSO2 Identity Server. I already knew the theory of OAuth2 and OIDC reasonably well. What I did not expect was how much I would learn from the gap between “the spec says this” and “WSO2 actually does this.” This post is a walkthrough of what I built and more honestly, a log of the moments where my assumptions were wrong.&lt;/p&gt;

&lt;p&gt;Before I get into it, a quick refresher in case some of this is new to you. WSO2 Identity Server is what is called an identity provider. Instead of every application in a company writing its own login page and its own password database, applications hand authentication off to one central server and that server issues a signed token as proof of who the user is. OIDC (OpenID Connect) is the standard that defines how that login handoff happens and it is built on top of OAuth2, which is really a standard about authorizing access to resources rather than logging people in. The practical result for a project like mine is two different tokens coming out of the same login, an ID token, which tells the frontend who just logged in and an access token, which the frontend then presents to the backend API as proof that it is allowed to make a given request. Keeping those two tokens straight and not just using whichever one happened to work first, turned out to be a running theme in this project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why an audit trail and why make it this strict
&lt;/h3&gt;

&lt;p&gt;The premise I picked for myself was simple. In a payment gateway, when a refund gets issued or a transaction gets flagged as fraud, someone eventually asks who did that, when and under what authority. In a lot of real systems the honest answer is “we are not sure, the logs are incomplete or they were editable after the fact.” I wanted to build something where that answer is never acceptable and to do it in a way that is enforced by the system itself rather than by policy or good intentions.&lt;/p&gt;

&lt;p&gt;That led to three non-negotiable design goals.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every audit record has to carry an identity that came from a signed token and never from a request field the client could fake.&lt;/li&gt;
&lt;li&gt;Once a record is written, nothing, not even a bug in my own code should be able to change or delete it.&lt;/li&gt;
&lt;li&gt;Different roles should only be able to do what their role permits, enforced at the API layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else in the project, the database trigger, the WSO2 configuration and the Spring Security setup exists to serve those three points.&lt;/p&gt;

&lt;h3&gt;
  
  
  The stack
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Spring Boot 3.5&lt;/strong&gt; and &lt;strong&gt;Java 17&lt;/strong&gt; for the backend, &lt;strong&gt;PostgreSQL 16&lt;/strong&gt; for storage, &lt;strong&gt;Flyway&lt;/strong&gt; for migrations, and &lt;strong&gt;WSO2 Identity Server 7.3&lt;/strong&gt; as the identity provider. Later I added a React frontend using &lt;strong&gt;react-oidc-context&lt;/strong&gt; and &lt;strong&gt;Tailwind&lt;/strong&gt; , mostly so I would have to deal with the SPA side of &lt;strong&gt;OAuth2&lt;/strong&gt; too, not just the resource server side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the database itself refuse to lie
&lt;/h3&gt;

&lt;p&gt;Before touching identity at all, I wanted the storage layer to be tamper-proof on its own terms. I did this in two ways.&lt;/p&gt;

&lt;p&gt;First, the application connects to Postgres as a role that is only ever granted SELECT and INSERT on the audit table. Postgres roles are database-level user accounts with their own permissions, separate from anything the application code decides. So this is not that UPDATE and DELETE are blocked by application logic, it is that the database user the app connects as literally does not have the privilege to run those statements, full stop, regardless of what the Java code above it tries to do. If someone found an SQL injection bug in my code tomorrow, the database itself would refuse the write, before my own validation logic ever gets a say. This is how I bring “security by design” to the database.&lt;/p&gt;

&lt;p&gt;Second, on top of that I added a PostgreSQL trigger, which is a small piece of database-side logic that runs automatically whenever a certain kind of statement is attempted. Mine raises an exception on any UPDATE or DELETE against the audit table, no matter who is asking. This is redundant with the privilege restriction on purpose. If I ever grant myself elevated privileges for some debugging session and forget to revoke them, the trigger is still there as a second, independent barrier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyyadtj4ebelclswzvysk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyyadtj4ebelclswzvysk.png" width="657" height="121"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;trigger rejection&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This part of the project felt satisfying in a very engineering-oriented way. Immutability is not a promise here, it is a property you can go try to break and fail to break.&lt;/p&gt;
&lt;h3&gt;
  
  
  WSO2 and where my assumptions started breaking
&lt;/h3&gt;

&lt;p&gt;This is the part of the project where I actually learned something new, so I want to spend most of this post here.&lt;/p&gt;

&lt;p&gt;My first mental model was that once I registered an application in WSO2 and assigned a user to a role, every token WSO2 issued for that user would naturally include that role. That is not how it works by default. Both the ID token and the access token are JWTs (JSON Web Tokens), which are just a &lt;strong&gt;signed,&lt;/strong&gt;  &lt;strong&gt;base64-encoded&lt;/strong&gt;  &lt;strong&gt;block of key-value pairs called claims&lt;/strong&gt; , things like who the user is, when the token expires and in this case, what roles they hold. But WSO2 does not automatically decide which claims go into which token. It has a “User Attributes” tab on each application where you decide, attribute by attribute, whether it goes into the ID Token, the Access Token or both. I had roles ticked for the ID Token only, because that is the token I was decoding and reading claims from during early testing. My access tokens were coming back completely clean of role information and for a while I genuinely thought the role assignment itself had not worked, when really the assignment was fine and only the token configuration was incomplete.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7015f69nfpmtr9p7bih1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7015f69nfpmtr9p7bih1.png" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx6vfquafgl97qoo2kk6e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx6vfquafgl97qoo2kk6e.png" width="799" height="548"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Roles enabled for ID token Access Token&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix, once I found the right tab was one checkbox. But it took a fair amount of decoding JWTs by hand at jwt.io and comparing claim sets between the two tokens before I isolated it.&lt;/p&gt;
&lt;h3&gt;
  
  
  One set of roles works everywhere
&lt;/h3&gt;

&lt;p&gt;I had originally created my FRAUD_ANALYST and COMPLIANCE_OFFICER roles while testing with a Password Grant application. Password Grant is an older OAuth2 flow where the client collects the username and password directly and exchanges them for a token, which is fine for quick testing with curl or Postman but not something you would expose to a real browser-based app, since it means the frontend handles raw credentials. Those roles worked fine there. Then I registered a second application, a proper Single-Page Application client using Authorization Code with &lt;strong&gt;PKCE&lt;/strong&gt; (the flow meant for browser apps, where the user is redirected to WSO2's own login page instead of typing their password into your app, and a generated secret called a code verifier prevents the returned code from being stolen and reused), for the actual React frontend. Suddenly the same users, logged into the same identity server, were only showing a generic everyone role, with FRAUD_ANALYST nowhere to be found.&lt;/p&gt;

&lt;p&gt;The reason turned out to be something called role audience. In WSO2 IS 7.3, when you create a role you have to choose whether it belongs to a single application or to the whole organization. An application-scoped role is only ever visible to the one application it was created under, even if the same user is logged in through a different application entirely. My original roles were scoped to the first app I created, so the second app genuinely could not see them, not because of a misconfiguration in the traditional sense, but because I had unknowingly designed for a single-application assumption that stopped being true the moment I added a second client.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1uwqqt914oyjn8efyr0f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1uwqqt914oyjn8efyr0f.png" width="799" height="224"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Roles set to organization&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix was to recreate the roles with an Organization audience and set the SPA application’s own Role Audience setting to match. Once I did that, both applications could see the same roles, assigned to the same users, which is obviously what I wanted from the start, I just had not understood that audience was a concept I needed to decide on deliberately.&lt;/p&gt;

&lt;p&gt;I will admit the first time I made that change, I did not actually see it work, and for a slightly embarrassing reason. I updated the role settings in the WSO2 console, went back to the browser tab where I was already logged into the React app, and reloaded the page. The console log still showed the old everyone-only role. My first thought was that the fix had not taken effect at all. What had actually happened is that reloading the page does not get you a new token, it just reuses the one already sitting in the browser session from before I made any changes. I needed to log out and log back in so WSO2 would issue a brand new token with the updated role information baked in. Once I did that, the correct role showed up immediately. It was a good reminder that a token is a snapshot taken at login time, not something that updates live just because the underlying configuration changed.&lt;/p&gt;
&lt;h3&gt;
  
  
  An access token is always a JWT
&lt;/h3&gt;

&lt;p&gt;Once roles were flowing correctly into the access token as a claim, I switched my frontend to send the access token instead of the ID token as the API bearer credential, which is the architecturally correct thing to do (ID tokens authenticate the user to the client app, access tokens authorize API calls and I had been quietly conflating the two for convenience). The backend immediately started rejecting every request with a 401.&lt;/p&gt;

&lt;p&gt;Turning on debug logging in Spring Security showed the real reason instead of my own generic error message: JOSE header typ (type) at+jwt not allowed. Every JWT has a small header before the actual claims, which includes a typ field describing what kind of token this is. Spring Security's auto-configured JWT decoder, the piece of the backend responsible for checking a token's signature and structure before trusting anything inside it, only accepts a typ of JWT or no typ header at all and rejects anything else immediately, before it even attempts to verify the signature. WSO2, however, issues access tokens with a JOSE header type of at+jwt, following RFC 9068, which is a standard specifically written to distinguish access tokens that happen to be JWTs from ID tokens and other generic JWTs, precisely so that a server cannot accidentally treat one as the other. Spring's defaults simply had not caught up to that distinction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88wmbtmqde6xhhdhj8bj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F88wmbtmqde6xhhdhj8bj.png" width="798" height="131"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;at+jwt not allowed&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix was to stop relying on Spring’s auto-configuration and build the JwtDecoder bean explicitly, widening the accepted JOSE types to include at+jwt. It is a small amount of code, but it only exists because I would not have known to look for it without reading the actual RFC that WSO2 was quietly following.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;JwtDecoder&lt;/span&gt; &lt;span class="nf"&gt;jwtDecoder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;NimbusJwtDecoder&lt;/span&gt; &lt;span class="n"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NimbusJwtDecoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withJwkSetUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jwkSetUri&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jwtProcessorCustomizer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setJWSTypeVerifier&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DefaultJOSEObjectTypeVerifier&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;JOSEObjectType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"at+jwt"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;JOSEObjectType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setJwtValidator&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JwtValidators&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createDefaultWithIssuer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;issuerUri&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Small landmine: the shape of the roles claim itself
&lt;/h3&gt;

&lt;p&gt;Even after all of the above, I found that the roles claim was not always the same shape, meaning the actual JSON type of the value changed depending on configuration, not just its contents. With application-scoped roles it arrived as a single comma-separated string, like "FRAUD_ANALYST". With organization-scoped roles it arrived as an actual JSON array, like ["FRAUD_ANALYST", "everyone"]. My original Spring Security converter, the piece of code responsible for turning raw token claims into permissions Spring understands, only handled the string case, splitting it on commas. Given an array instead, it silently produced zero permissions rather than throwing an error, which is a particularly unpleasant kind of bug because it does not look like a bug. It looks like a working authorization check that happens to always say no. I ended up rewriting the converter to check the actual type of the claim first and normalize either shape into the same internal list, rather than assuming one shape and hoping.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one landmine that was not WSO2’s fault at all
&lt;/h3&gt;

&lt;p&gt;Not every problem I hit was WSO2 being unexpected. One of them was entirely mine. WSO2 Identity Server 7.3 requires Java 21 to run, but my Spring Boot backend targets Java 17, so in the same terminal session I would switch JAVA_HOME to point at Java 21 to start WSO2, and then needed to switch it back to Java 17 before starting the backend. One session, mid debugging, I forgot the second half of that. The backend started fine under Java 21 instead of 17, connected to the database fine and then failed the moment it tried to fetch WSO2's signing keys, with a wall of PKIX path building failed: unable to find valid certification path to requested target.&lt;/p&gt;

&lt;p&gt;That error is Java telling you it does not trust the certificate on the other end of an HTTPS connection. Earlier in the project I had already imported WSO2’s self-signed certificate into the Java 17 truststore so the backend could talk to WSO2 over HTTPS locally. I had done that for Java 17 specifically, because that is the JDK the backend normally runs on. Running the exact same backend code under Java 21 instead meant it was reaching for a completely different truststore, one that had never seen that certificate, so the TLS handshake failed before a single line of my own code ran.&lt;/p&gt;

&lt;p&gt;Nothing was actually broken. My code was correct, WSO2 was correct, the certificate was correctly imported, just into the wrong JDK’s truststore because I was running the wrong JDK. The fix was to explicitly set JAVA_HOME back to the Java 17 install before starting the backend again. It was a good reminder that a lot of "mysterious" failures in a project with two different Java versions in play are not mysterious at all, they are just a matter of confirming which JDK is actually running right now, not which one you assume is running.&lt;/p&gt;

&lt;h3&gt;
  
  
  RBAC at the endpoint level
&lt;/h3&gt;

&lt;p&gt;None of the identity work matters if the API does not actually enforce anything with it. This is where RBAC, role-based access control, actually earns its name. It is the idea that permissions are attached to roles rather than to individual users, so instead of checking “is this specific person allowed to do this,” the system checks “does this person hold a role that is allowed to do this.” Every endpoint in the controller is guarded with @PreAuthorize, a Spring Security annotation that runs a permission check before the method body executes at all, against the specific role that should be allowed to call it. A fraud analyst can flag a transaction but cannot pull the compliance dashboard. A compliance officer can view the dashboard and search events but cannot record a fraud flag. And the performedBy field on every audit record is read from the authenticated principal (Spring Security's term for "the verified identity attached to this request"), taken from the sub claim on the verified token, never accepted as a plain value the client supplies in the request body. That last part matters more than it sounds: if performedBy were just another field in the request, anyone could claim to be anyone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw4vfbn84evb1luiz0i82.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw4vfbn84evb1luiz0i82.png" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcnux3mn9j08lq2hy6xfk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcnux3mn9j08lq2hy6xfk.png" width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;201 and 403 errors&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The frontend and seeing the roles work end to end
&lt;/h3&gt;

&lt;p&gt;Building the React frontend was, in a strange way, the moment all of the WSO2 debugging actually paid off in something visible. Logging in as sara and seeing the fraud analyst actions render, then logging in as joe and seeing the compliance summary cards render instead, made the whole chain of trust click into place in a way that reading JWT claims in a terminal never quite did.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fesra1hmh6u7csc5secl0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fesra1hmh6u7csc5secl0.png" width="800" height="571"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;dashboard-sara&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcc00ah1jdk4asmkp6pt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcc00ah1jdk4asmkp6pt.png" width="800" height="567"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;dashboard-joe&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What I would tell someone starting this from scratch
&lt;/h3&gt;

&lt;p&gt;If I had to compress everything above into advice for my own past self, it would be this. Do not assume a claim exists on a token just because you configured it somewhere, decode the actual token and check. Decide on role audience deliberately before you have more than one application, because retrofitting it later means recreating roles and reassigning users. And when Spring Security gives you a vague 401, turn on debug logging before you guess, because the real reason is usually one specific line away, not a deep mystery.&lt;/p&gt;

&lt;p&gt;Building this project did not make WSO2 feel like a black box to me anymore. It made it feel like a system with a lot of very deliberate, very specific decisions baked into it, most of which are invisible until you hit them directly. That, more than the finished dashboard, is what I actually wanted out of building this.&lt;/p&gt;

</description>
      <category>oauth2</category>
      <category>springboot</category>
      <category>softwareengineering</category>
      <category>wso2</category>
    </item>
    <item>
      <title>How I Ran WSO2 Identity Server on AWS Without Spending a Cent</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Tue, 23 Jun 2026 18:02:48 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/how-i-ran-wso2-identity-server-on-aws-without-spending-a-cent-27i2</link>
      <guid>https://dev.to/pasindu_balasooriya/how-i-ran-wso2-identity-server-on-aws-without-spending-a-cent-27i2</guid>
      <description>&lt;p&gt;&lt;em&gt;A broke man’s guide to running enterprise grade identity software on AWS&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1g3fbjejs6xidmipxyj5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1g3fbjejs6xidmipxyj5.png" width="409" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A little while ago I set myself a challenge. I wanted to take a serious piece of identity software, the kind big companies use to handle everyone’s logins and run it properly on Amazon’s cloud. Not a cut down demo on my laptop but the real thing. Two servers sitting in two different data centers, a proper database behind them, firewalls, monitoring and alarms. And the whole thing had to cost me nothing.&lt;/p&gt;

&lt;p&gt;This is the story of how I got there. I have tried to keep it plain and honest including the bits where things went wrong, because that is usually where the learning actually happens.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I was actually building
&lt;/h3&gt;

&lt;p&gt;The software at the center of all this is called &lt;strong&gt;WSO2 Identity Server&lt;/strong&gt;. Think of it as a receptionist whose only job is checking who you are. Instead of every app you build handling its own usernames and passwords, they all ask this one server whether you really are who you claim to be and whether you are allowed in. It is the same idea as the “Sign in with Google” button, except here I am running the identity server myself.&lt;/p&gt;

&lt;p&gt;The goal was to wrap that receptionist in a proper, safe building. One public front door, two receptionists in case one falls ill, a locked records room for the data, cameras watching everything and a guard who turns away anyone poking at the staff entrance. Most tutorials skip all of that and just put the receptionist on the pavement. I wanted the whole building.&lt;/p&gt;

&lt;p&gt;And I wrote every piece of it as code &lt;strong&gt;(IaC)&lt;/strong&gt;, using a tool called &lt;strong&gt;Terraform,&lt;/strong&gt; so I can build the entire thing with one command and tear it all down again with another.&lt;/p&gt;

&lt;h3&gt;
  
  
  The money rule that shaped everything
&lt;/h3&gt;

&lt;p&gt;Here is the constraint that made this interesting. Newer AWS accounts come with a free plan that hands you some credits, somewhere around a hundred to two hundred dollars worth and the lovely part is that the account simply cannot be charged. When the credits run out Amazon pauses things rather than sending you a bill. So “no cost to me” was a real guarantee and not wishful thinking.&lt;/p&gt;

&lt;p&gt;The twist is that those credits still drain by the hour while things are switched on. So the trick was never to leave it all running. I would build the whole setup, test it, take my screenshots and then destroy it again, every single session. Because it is all written as code, bringing it back is one command and about ten minutes. The entire project cost me a few dollars of credits in the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 0
&lt;/h3&gt;

&lt;p&gt;Before any servers existed I set up the environment that saves you later. I created a &lt;strong&gt;budget alarm&lt;/strong&gt; that emails me if spending ever climbs, which on the free plan is more of a comfort blanket than a real risk. I set up a safe, encrypted place for Terraform to keep track of what it has built. And I copied the two big installers, &lt;strong&gt;the WSO2 software&lt;/strong&gt; and the &lt;strong&gt;Java&lt;/strong&gt; it runs on into Amazon storage once, so that every rebuild pulls them quickly from inside AWS instead of dragging them across the internet again. All of it makes every later step faster and calmer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1
&lt;/h3&gt;

&lt;p&gt;This is the part people skip and later regret. I carved out my own private slice of Amazon’s network and split it into three layers. A &lt;strong&gt;public layer&lt;/strong&gt; where the front door lives. A &lt;strong&gt;private layer&lt;/strong&gt; for the servers. And a sealed off layer for the database that has no route to the internet at all.&lt;/p&gt;

&lt;p&gt;Then I put firewalls between them. The front door only accepts secure web traffic from the outside world. The servers only accept traffic from the front door. The database only accepts traffic from the servers. Each layer can only talk to the next one in line.&lt;/p&gt;

&lt;p&gt;The mental model that finally clicked for me goes like this. There are two kinds of firewalls here. One is fine grained and controls exactly who may talk to whom. The other is a blunt instrument that seals off an entire zone. You really want both and now I understand why.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2
&lt;/h3&gt;

&lt;p&gt;Next came the database, a managed &lt;strong&gt;MySQL database&lt;/strong&gt; from Amazon. It sits in that sealed off layer and I made sure of two things. It has no public address so I genuinely cannot reach it from my laptop, which is exactly the point. And its passwords are generated automatically and kept in a digital vault called &lt;strong&gt;Secrets Manager&lt;/strong&gt; , never written into any file or into the code. The servers fetch them only when they start up.&lt;/p&gt;

&lt;p&gt;There was a neat side effect. Because the database is not reachable from outside, I could not set up its tables from my laptop either. That job had to be done from inside the network, which became a task for the next phase.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzt8l8tdqmvg3106maxak.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzt8l8tdqmvg3106maxak.png" width="800" height="403"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1. The database running privately inside AWS&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkr56m91j1sld90qt0gvw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkr56m91j1sld90qt0gvw.png" width="800" height="404"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2. The database instance as it appears in the AWS console&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3
&lt;/h3&gt;

&lt;p&gt;Now the fun part. I wanted a server that could build itself from nothing. Here is the flow I automated. A plain Amazon Linux machine starts up empty. A tiny startup note tells it to fetch a larger setup script from storage and run it. That script installs Java, unpacks WSO2, grabs the database password from the vault and writes out the server’s configuration with the right database address and credentials already filled in. Then it connects to the database, sets up the tables and starts the server.&lt;/p&gt;

&lt;p&gt;One thing I am quietly proud of is that there is no SSH involved anywhere. To get a shell on a server I use &lt;strong&gt;Amazon’s Session Manager&lt;/strong&gt; , so there is no open remote login port and no key for me to lose.&lt;/p&gt;

&lt;p&gt;Then came the satisfying moment of proof. The server logged that it had started in 60 seconds. Its health check came back healthy. The database now held well over 200 tables which proved the server was really using the cloud database and not some throwaway local one. And there were zero connection errors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6e8jwxnavxhg8jtsuymp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6e8jwxnavxhg8jtsuymp.png" width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3. The server up and running in AWS&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The plot twist nobody warns you about
&lt;/h3&gt;

&lt;p&gt;My very first attempt to launch a server failed. The error simply said the server type I had chosen was not eligible for the free plan.&lt;/p&gt;

&lt;p&gt;It turns out the free plan only lets you launch certain blessed server types. The obvious small one I picked was not on the list. And the truly tiny ones that are on the list have only 1 GB of memory, which is nowhere near enough for WSO2 which wants at least 4 GB.&lt;/p&gt;

&lt;p&gt;The way out took some digging. Amazon has newer flex servers that are on the eligible list and come with real memory. One of them has 4 GB and another has 8 GB. I went with the 8 GB one and suddenly everything fit, still at no cost. This is the kind of thing you only ever learn by walking straight into it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 4
&lt;/h3&gt;

&lt;p&gt;A single server is not high availability. So I added a second one in the other data center and put a &lt;strong&gt;load balancer&lt;/strong&gt; in front of both. The load balancer is the single public web address that everyone visits. It quietly checks both servers every 30 seconds and only sends visitors to a healthy one. It also keeps each visitor stuck to the same server during their session so that logins flow smoothly.&lt;/p&gt;

&lt;p&gt;I also gave it a certificate so all the traffic is encrypted. I do not own a domain name for this yet, so the certificate is a self-made one which means browsers show a warning even though the encryption itself is real. Swapping in a proper certificate later is a tiny change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwfapj6xfly9m580rrqvo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwfapj6xfly9m580rrqvo.png" width="799" height="402"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4. The load balancer — the single front door&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I made the servers fully self-sufficient too. Each one when it boots, installs itself, sets up the database if it happens to be the first to arrive and starts serving. No manual steps. That is the thing that makes the idea of killing a server and having a fresh one quietly take its place actually work.&lt;/p&gt;

&lt;h3&gt;
  
  
  The clustering headache
&lt;/h3&gt;

&lt;p&gt;The two servers need to find each other and behave as one system, so that a change made on one is known to the other. WSO2 has a mode that is supposed to discover the other servers on AWS automatically. It quietly did nothing. The library that powers that feature is not included in this version, so each server thought it was completely alone. Two servers, each convinced it was the only one.&lt;/p&gt;

&lt;p&gt;To fix this I had each server ask Amazon directly at startup, which servers carry the same cluster label and then configure itself from that answer. After that change the logs showed exactly what I wanted. One server was elected as the leader and the other joined it. One cluster and two members, working as a pair.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proof it all works
&lt;/h3&gt;

&lt;p&gt;Both servers showed up as healthy behind the load balancer, one in each data center. Visiting the public address took me to the login page over a secure connection, the admin console loaded and the standard identity document was served correctly. A real request through the load balancer using the admin account came back successful and a request with a wrong password was correctly refused. So the whole chain works from the public internet, through the load balancer, into a server and down to the database with real authentication at the end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwcfh88xchnp3jcoxaa5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwcfh88xchnp3jcoxaa5.png" width="799" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 5. The admin console, served through the load balance&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhiz7sv3n2r1fwiql7vo3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhiz7sv3n2r1fwiql7vo3.png" width="800" height="417"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 6. The identity server publishing its OpenID configuration&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcrvy6pqp9iclladz021i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcrvy6pqp9iclladz021i.png" width="800" height="402"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 7. The identity server publishing its OpenID configuration&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 5
&lt;/h3&gt;

&lt;p&gt;A system you cannot see into is a liability, so I wired up monitoring. A small agent on each server ships the important logs up to Amazon’s cloud logging. The main server log, the audit log that records who logged in and who failed, and the web access log.&lt;/p&gt;

&lt;p&gt;Then I set up alarms that watch those logs and the load balancer for trouble. A sudden spike of errors. A burst of failed logins, which is the classic sign of someone guessing passwords. Server errors, an unhealthy server or a database working too hard. Each one sends me an email.&lt;/p&gt;

&lt;p&gt;Finally, I built a dashboard that pulls it all together in one place, so I can glance at traffic, the number of healthy servers, memory use, database load and a live count of failed logins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9m0vdgbvnokz14exktz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk9m0vdgbvnokz14exktz.png" width="799" height="377"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 8. The monitoring dashboard&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqdlw85yax4jdr5j3bbw5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqdlw85yax4jdr5j3bbw5.png" width="800" height="406"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 9. The alarms standing guard&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2mvsx61zsaac164zjnr4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2mvsx61zsaac164zjnr4.png" width="800" height="402"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 10. The CloudWatch overview, every alarm green and watching quietly&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I tested the whole alert chain from start to finish, and a real test email landed in my inbox. So I knew that when I later attacked my own system, the failed login alarm would actually fire. That alarm is the tripwire the attack demo would spring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 6
&lt;/h3&gt;

&lt;p&gt;Up to this point the admin console was reachable by anyone on the internet which is not acceptable for the control panel of an identity system. So I closed it off.&lt;/p&gt;

&lt;p&gt;The load balancer now checks where each request is coming from. The admin pages only answer if the request comes from my own internet address. Everyone else gets a flat refusal, a plain forbidden response. The normal login pages stay public, because they have to be. I tested it both ways. It loads for me and it slams shut for everyone else.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9e6xopcpyyidewia4i7w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9e6xopcpyyidewia4i7w.png" width="800" height="417"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 11. Anyone else trying the admin console gets turned away&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I also built a web firewall that I can switch on, which blocks common attacks and more importantly for what came next, limits how fast a single attacker can hammer the login. I tightened what the servers themselves are allowed to reach out to, down to only the things they genuinely need. And I turned on account lockout so that after a handful of wrong passwords an account locks itself for a while, even if the next guess happens to be correct.&lt;/p&gt;

&lt;p&gt;There is one real world wrinkle worth admitting. My home internet address changes every so often and the console lock is pinned to it. So now and then I have to update one line and apply it again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phases 7 and 8
&lt;/h3&gt;

&lt;p&gt;This is the part I built the whole thing for. Two demonstrations. First, a real person logging in. Then, proof that the system shrugs off both a server dying and an actual attack.&lt;/p&gt;

&lt;h3&gt;
  
  
  A real login
&lt;/h3&gt;

&lt;p&gt;I built a tiny demo app that signs a user in through the identity server using the standard &lt;strong&gt;OpenID Connect&lt;/strong&gt; flow and then shows the identity it got back. I registered the app, created a test user and logged in. The app received a signed token and when I looked inside it, the token had genuinely been issued by my own server, for my own test user. Logins work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkigqu2b92vkd7w4w96zu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkigqu2b92vkd7w4w96zu.png" width="799" height="423"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 12. The little demo app, signed in through my identity server&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmf926j93vkonpy4dfk7k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmf926j93vkonpy4dfk7k.png" width="800" height="417"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 13. Registering the demo app inside WSO2&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbv9wp6hijllqm97542tn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbv9wp6hijllqm97542tn.png" width="800" height="419"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 14. The sign in options switched on for the app&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Killing a server in the middle of everything
&lt;/h3&gt;

&lt;p&gt;Then the honest stress test. While logins were happening, I abruptly killed one of the two servers. The result was exactly what real high availability looks like, resilient. A couple of requests stumbled for a few seconds while the load balancer noticed the server was gone. Then every login succeeded again on the survivor. And the system healed itself. A fresh server was launched to replace the dead one all on its own, back to two servers across two data centers without me lifting a finger. Because the servers keep no unique data of their own and everything lives in the shared database, losing one lost nothing.&lt;/p&gt;

&lt;p&gt;While that server was down, the monitoring did its job too and the unhealthy server alarm dropped into my inbox without me asking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3odofkruz3y7nng3v2gi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3odofkruz3y7nng3v2gi.png" width="799" height="413"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 15. The unhealthy server alarm arriving by email&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Attacking my own login
&lt;/h3&gt;

&lt;p&gt;Finally, the fun one. I pointed a script at my own login page and hammered it with around 150 wrong passwords, a small taste of what a real password guessing attack looks like. Then I sat back and watched every layer of defense catch it.&lt;/p&gt;

&lt;p&gt;The account locked itself. After a handful of wrong tries the target account was locked and from then on even the correct password was refused. So the attacker is stopped even if they eventually guess right.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9mraxfe63wpx6cp5xib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr9mraxfe63wpx6cp5xib.png" width="800" height="419"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 16. The account locking itself after too many wrong tries.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It was all recorded. Every failed attempt flowed up into cloud logging and a counter I had set up tallied 55 failures.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6aj9yiww0sf5bo411cr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg6aj9yiww0sf5bo411cr.png" width="800" height="407"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 17. Failed logins piling up in the logs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An alarm fired and emailed me. The too many failed logins alarm tripped and sent a real warning email, the detection layer working live in front of my eyes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqoy731dbtzbrnqz2l9p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqoy731dbtzbrnqz2l9p.png" width="800" height="399"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 18. The alarm going off&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7582a1dlcftpiphksp9p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7582a1dlcftpiphksp9p.png" width="799" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 19. And the warning landing in my inbox&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The firewall blocked the flood. Once the attacker crossed the speed limit, the web firewall started turning requests away at the very edge before they ever reached a server. The firewall’s own console tells the story plainly. Out of 535 requests in that window it let 385 through and blocked 150 of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fofj4dg190d2f6s6k41wb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fofj4dg190d2f6s6k41wb.png" width="800" height="233"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 20. The firewall’s running tally of allowed and blocked requests&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can even see where the blocking happens. The traffic flows into the rate limit rule and a thick slice of it is stopped right there while the rest carries on to the servers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvgn6i9ifa0ddmzp96m3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvgn6i9ifa0ddmzp96m3.png" width="799" height="384"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 21. The traffic hitting the rate limit rule and a slice being blocked&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The spikes in blocked traffic line up exactly with my two attack bursts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft979x8uog23lnpgy86m6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft979x8uog23lnpgy86m6.png" width="799" height="370"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 22. Allowed and blocked totals spiking during the attack&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuq1waby3ma7ww6vycgqo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuq1waby3ma7ww6vycgqo.png" width="799" height="373"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 23. The rate limit rule doing the blocking during the bursts&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The firewall could even tell that a good slice of this traffic was not a real browser at all. It flagged around 160 of the requests as unverified bot activity rather than genuine visitors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyvqs49bg3kqoo59qndo2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyvqs49bg3kqoo59qndo2.png" width="800" height="377"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 24. The firewall flagging the attack as unverified bots&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is the wider view of everything the firewall was seeing across that traffic, and a closer look at the individual requests it turned away.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffybsnfk2t8r6keafcl35.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffybsnfk2t8r6keafcl35.png" width="800" height="396"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 25. the firewall’s traffic overview&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs8zsnbg03th0ws7sjssz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs8zsnbg03th0ws7sjssz.png" width="799" height="398"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 26. A closer look at the requests it turned away&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three separate layers, the app locking accounts, the cloud watching and alerting and the firewall blocking at the door, each caught the same attack in its own way. That is what people mean by defense in depth, shown working rather than just claimed.&lt;/p&gt;

&lt;p&gt;I want to be honest about one thing here, because it is the most useful lesson of the lot. My failed login alarm did not work on the first try. My first guess at what a failed login looks like in the logs was simply wrong, so the alarm sat there doing nothing. I only found the real signal by running the attack and reading the actual log lines it produced and then fixing the alarm to match. You cannot trust a detector you have never tested.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9jd60xnidx95jdxok66.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy9jd60xnidx95jdxok66.png" width="798" height="145"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 27. The real audit log lines from the attack, the ones I had to learn to read&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0bmpfwsuhrgr19c3obgd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0bmpfwsuhrgr19c3obgd.png" width="800" height="619"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 28.Reading the real log lines and fixing the detector to match&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What it cost and what I walked away with
&lt;/h3&gt;

&lt;p&gt;The whole thing ran on free credits. While it is switched on it costs a few cents an hour and switched off, which is how it sits between sessions, it costs essentially nothing. The entire project came to a few dollars of credits and the free plan could not have charged me even if I had forgotten to turn it off.&lt;/p&gt;

&lt;p&gt;A few lessons really stuck with me. The two kinds of firewall finally make sense, one for fine control and one for sealing off whole zones and you want both. The free tier has sharp edges and it will not let you launch ordinary servers, which you find out by walking into the wall. The phrase about clustering happening automatically often means it does nothing of the sort and you end up wiring it together yourself. High availability is not instant and a dying server causes a few seconds of wobble before things settle. And you genuinely cannot trust a security alarm you have not tested against a real attack.&lt;/p&gt;

&lt;p&gt;If I were to take this further, I would add a proper domain name and certificate, build a ready-made server image so new servers start in seconds instead of minutes and double up the networking so even that has no single point of failure. Those are the natural steps from a great learning lab towards something you would actually run in production.&lt;/p&gt;

&lt;p&gt;Everything I built is written down as code in the repository, every firewall rule, every config file, every demo script. If you fancy it, you can stand the whole thing up, attack it yourself and tear it back down. That honestly, was the best way I have ever found to actually learn this stuff.&lt;/p&gt;

&lt;p&gt;The entire stack is open source on GitHub at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pasindubalasooriya/wso2-is-aws" rel="noopener noreferrer"&gt;GitHub - pasindubalasooriya/wso2-is-aws: Highly Available WSO2 Identity Server 7.3 on AWS (2 AZs) in Terraform - ALB, RDS, clustering, observability &amp;amp; WAF.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is all Terraform, split into modules for the network, the database, the compute fleet, the load balancer, the monitoring and the firewall, alongside the cloud init scripts and the attack demo. Run terraform apply to stand the whole thing up in about ten minutes and terraform destroy to wipe it back to nothing.&lt;/p&gt;

</description>
      <category>wso2</category>
      <category>wso2is</category>
      <category>aws</category>
      <category>opensource</category>
    </item>
    <item>
      <title>eu-west-2 Is Not a Compliance Strategy</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Fri, 05 Jun 2026 13:32:12 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/eu-west-2-is-not-a-compliance-strategy-3jam</link>
      <guid>https://dev.to/pasindu_balasooriya/eu-west-2-is-not-a-compliance-strategy-3jam</guid>
      <description>&lt;p&gt;&lt;em&gt;Selecting the AWS London region is one decision. Keeping patient data in the UK is twenty&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ots8dbt1dfqkxx34auf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ots8dbt1dfqkxx34auf.png" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a common misconception in UK healthcare IT that data residency is a configuration choice. You pick eu-west-2 during account setup and move on.&lt;/p&gt;

&lt;p&gt;It is not that simple. And in a sector governed by UK GDPR, the Data Protection Act 2018 and the NHS Data Security and Protection Toolkit, the gap between believing you are compliant and actually being compliant can be the difference between a clean audit and a reportable breach.&lt;/p&gt;

&lt;h4&gt;
  
  
  What the region selection covers
&lt;/h4&gt;

&lt;p&gt;When you provision AWS resources in eu-west-2, you are telling AWS to physically locate those resources in their London data centres. Data stored in an S3 bucket provisioned in eu-west-2 does not replicate to other regions by default. An RDS database provisioned in eu-west-2 runs on hardware in the UK. The underlying infrastructure stays within UK jurisdiction.&lt;/p&gt;

&lt;p&gt;That is meaningful. But it is also the easiest part.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where the gaps appear
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Cross-region replication is opt-in, but so is preventing it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;S3 Cross-region Replication is disabled by default, but it only takes one misconfigured replication rule, one automated backup policy copying to us-east-1 for cost reasons or one CloudTrail log bucket pointing to a cheaper non-UK region to put patient identifiable information outside UK borders. The region selection does not prevent any of this. Only deliberate Service Control Policies (SCPs) at the AWS Organisation level can enforce that no resource is ever created outside eu-west-2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed services have regional defaults you need to verify individually.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Amazon SES, Lambda edge functions, CloudFront distributions and several analytics services have default behaviours that operate globally or default to US regions. Each one requires regional configuration. Assuming that provisioning within eu-west-2 cascades to every attached service is one of the most common architectural mistakes in cloud migrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC Endpoints are not automatic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without VPC Endpoints configured for S3 and other AWS services, traffic from your application tier to AWS managed services routes over the public internet even when both the application and the service are in eu-west-2. This is not a data residency failure, but it is a data-in-transit exposure that creates significant compliance problems under UK GDPR’s requirement for appropriate technical measures. VPC Endpoints ensure that traffic between services never leaves the AWS private network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CloudTrail logs have to be explicitly retained in-region.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AWS CloudTrail provides the immutable audit log that regulators and the NHS DSP Toolkit assessors will ask for. But CloudTrail logs can be configured to export to S3 buckets in whatever region is cheapest or most convenient. If your audit logs live in eu-west-1 or us-east-1, you have a data residency problem in the very system you rely on to prove you do not have a data residency problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywgzxe63ox5ypqbvjsm4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fywgzxe63ox5ypqbvjsm4.png" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The architecture that actually enforces residency
&lt;/h4&gt;

&lt;p&gt;Genuine UK data residency in AWS requires a layered approach.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;An &lt;strong&gt;SCP at the organisation level&lt;/strong&gt; denying any API call that would create resources outside eu-west-2.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A &lt;strong&gt;dedicated S3 bucket policy&lt;/strong&gt; blocking cross-region replication.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;3. VPC Endpoints&lt;/strong&gt; for every AWS service the application consumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. CloudTrail and CloudWatch log groups&lt;/strong&gt; explicitly provisioned in eu-west-2 with bucket policies preventing replication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. KMS key policies&lt;/strong&gt; scoped to the London region.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. CloudWatch alarm&lt;/strong&gt; that fires if any of these controls drift.&lt;/p&gt;

&lt;p&gt;None of this is complex. All of it is easy to miss if you treat region selection as the end of the compliance conversation rather than the beginning.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why this matters specifically in healthcare
&lt;/h4&gt;

&lt;p&gt;NHS England’s progressive migration to AWS and their maintenance of full DSP Toolkit compliance throughout, demonstrates that it is entirely achievable. But their architecture teams spent considerable effort on exactly these controls. The clinics, trusts and private healthcare providers following in their footsteps inherit the destination but not necessarily the map.&lt;/p&gt;

&lt;p&gt;Data residency in UK healthcare is not a setting. It is an architectural discipline. The earlier that is treated as a constraint rather than a checkbox, the less expensive it becomes to get right.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>data</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title>How I Swapped My Entire Database in One Afternoon</title>
      <dc:creator>Pasindu Balasooriya</dc:creator>
      <pubDate>Sun, 31 May 2026 18:21:26 +0000</pubDate>
      <link>https://dev.to/pasindu_balasooriya/how-i-swapped-my-entire-database-in-one-afternoon-492l</link>
      <guid>https://dev.to/pasindu_balasooriya/how-i-swapped-my-entire-database-in-one-afternoon-492l</guid>
      <description>&lt;p&gt;Most tutorials teach you how to add a database to a React app. Almost none teach you how to replace one. That’s a shame because the second skill is where the design gets interesting and it’s the one that quietly separates code that ages well from code that calcifies.&lt;/p&gt;

&lt;p&gt;I learned this the cheap way recently. Halfway through building a subscription tracker, I decided to move it off &lt;strong&gt;Cloud Firestore&lt;/strong&gt; and onto &lt;strong&gt;Firebase Realtime Database&lt;/strong&gt;. Two different databases and two different APIs. I braced for a long afternoon of find-and-replace across the whole codebase.&lt;/p&gt;

&lt;p&gt;It took about twenty minutes and I looked at four files. None of them were components.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F1024%2F0%2Abuy3NtaBptzeldAW" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F1024%2F0%2Abuy3NtaBptzeldAW" width="1024" height="1426"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Photo by charlesdeluvio on Unsplash&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  The thing nobody tells you about Firestore vs. Realtime Database
&lt;/h4&gt;

&lt;p&gt;On paper they are like siblings. Both live in Firebase, both sync in real time and both are “NoSQL.” In practice they disagree about almost everything that matters when you write code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reads.&lt;/strong&gt; Firestore gives you onSnapshot over a query. Realtime Database gives you onValue over a ref.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writes.&lt;/strong&gt; Firestore has addDoc / updateDoc / deleteDoc. Realtime Database has push / update / remove.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shape.&lt;/strong&gt; Firestore returns an array of documents. Realtime Database returns one big nested object keyed by push-IDs, and you have to walk it with snapshot.forEach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time.&lt;/strong&gt; This is the sneaky one. Firestore has a first-class Timestamp type with a .toDate() method. Realtime Database has no such thing and its serverTimestamp() resolves to a plain number of milliseconds since 1970.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last difference is exactly the kind of detail that turns a simple swap into a bug hunt. If you had been storing JavaScript Date objects and calling .toDate() on the way out, half your UI breaks silently the moment you switch.&lt;/p&gt;
&lt;h4&gt;
  
  
  The seam
&lt;/h4&gt;

&lt;p&gt;Somewhere early on, I had written a file called subscriptions.js whose entire job was to be the only place in the app that knew the database existed. Everything else such as every component, every screen talked to it through four functions below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;watchSubscriptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// returns an unsubscribe fn&lt;/span&gt;
&lt;span class="nf"&gt;addSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;updateSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;deleteSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the seam. A thin boundary between what my app wants (give me this user’s subscriptions, and tell me when they change) and how that happens (Firestore? Realtime Database? a REST API? localStorage?).&lt;/p&gt;

&lt;p&gt;People with a backend background will recognise this as a cousin of the &lt;strong&gt;repository pattern&lt;/strong&gt; or the data-access layer in hexagonal / ports-and-adapter_s_ architecture. But you do not need the vocabulary to get the benefit. You just need the discipline to never import firebase/firestore inside a component.&lt;/p&gt;

&lt;p&gt;The migration was almost entirely inside those four functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before — Firestore&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;onSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;onData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// after — Realtime Database&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;onValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;orderByChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;val&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="nf"&gt;onData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// RTDB orders ascending; flip for newest-first&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same signature in and same shape out, [{ id, ...fields }], newest first. The component consuming it is a React context with a live list and a loading flag that had no idea anything had changed. It still received an array. It still rerendered. The real time magic still worked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F723fd3nce4y4oqlq2wmu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F723fd3nce4y4oqlq2wmu.png" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The one leak — and why leaks are fine if you plan for them
&lt;/h4&gt;

&lt;p&gt;No abstraction is perfect and the Timestamp problem was the seam's one genuine leak. Realtime Database simply does not store a Date. I had to change the write side to store epoch milliseconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;renewalDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;renewalDate&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;renewalDate&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the read side I had a tiny helper from day one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toDate&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Firestore Timestamp&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ISO string OR epoch ms&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had written that else branch months earlier without much thought, just to be defensive about the different shapes a date might arrive in. It meant my entire formatting and days until renewal logic already handled raw numbers. The read side did not need a single edit. Defensive normalisation at the boundary had paid for itself.&lt;/p&gt;

&lt;h4&gt;
  
  
  The takeaway
&lt;/h4&gt;

&lt;p&gt;The seam is nott a framework or a library. It is a single rule applied early, when it feels like over-engineering.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The database gets exactly one doorway into your app. Everything passes through it, in your own vocabulary, in your own data shapes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It costs you almost nothing up front but one extra file and four functions. The payoff is asymmetric and arrives later usually on the day you are told to “just switch the backend real quick.” On that day, the difference between a twenty-minute change and a two-day rewrite is whether you drew the seam back when it was boring to do so.&lt;/p&gt;

</description>
      <category>firestore</category>
      <category>swap</category>
      <category>database</category>
      <category>firebase</category>
    </item>
  </channel>
</rss>
