<?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: RockAndNull</title>
    <description>The latest articles on DEV Community by RockAndNull (@rockandnull).</description>
    <link>https://dev.to/rockandnull</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F550287%2F12c9b389-1d76-4a8f-a97a-772343a78588.jpg</url>
      <title>DEV Community: RockAndNull</title>
      <link>https://dev.to/rockandnull</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rockandnull"/>
    <language>en</language>
    <item>
      <title>The danger of the "ok" project</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Tue, 31 Mar 2026 12:48:11 +0000</pubDate>
      <link>https://dev.to/rockandnull/the-danger-of-the-ok-project-4ae1</link>
      <guid>https://dev.to/rockandnull/the-danger-of-the-ok-project-4ae1</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%2Fimages.unsplash.com%2Fphoto-1527900887130-4c59133ff4a7%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fG1pZGRsZXxlbnwwfHx8fDE3NzQ5NjExNzd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1527900887130-4c59133ff4a7%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fG1pZGRsZXxlbnwwfHx8fDE3NzQ5NjExNzd8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="The danger of the " width="2000" height="1333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The hardest part of building a project isn’t always dealing with failure.&lt;/p&gt;

&lt;p&gt;It’s dealing with a project that is doing just well enough to keep going. There is a common situation in software where a project isn’t a total disaster, but it isn’t a hit either.&lt;/p&gt;

&lt;p&gt;It has some users and some traction, but not enough to be a clear success. This is the "middle ground," and as Dropbox founder Drew Houston once noted, it can be the most difficult place to be. If a project fails, the choice to stop is easy. If it’s a success, the choice to grow is easy. But when it’s just "okay," it stays alive and uses up energy without a certain future.&lt;/p&gt;

&lt;p&gt;In the AI era, the timing of this decision feels different because the industry moves so fast. A few years ago, it was common to let a project sit for a long time to see if it would eventually grow. Today, a project that is only "okay" now might be outdated in a very short time because tools and markets change every month. The "wait and see" approach carries a new kind of risk. The speed of the field makes the gap between a slow-growing project and a fast-moving market much more obvious.&lt;/p&gt;

&lt;p&gt;Deciding what to do next usually involves a mix of data and intuition.&lt;/p&gt;

&lt;p&gt;Some look at retention data to see if users are actually staying. It is a choice between waiting for more growth or deciding that the current traction isn't enough to justify more time. In a world that moves this fast, the middle point remains a challenging spot to navigate, with no clear data-driven/rational solution.&lt;/p&gt;

</description>
      <category>thoughts</category>
      <category>business</category>
    </item>
    <item>
      <title>Who is going to train the juniors?</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Thu, 05 Feb 2026 09:59:43 +0000</pubDate>
      <link>https://dev.to/rockandnull/who-is-going-to-train-the-juniors-3ha</link>
      <guid>https://dev.to/rockandnull/who-is-going-to-train-the-juniors-3ha</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%2Fimages.unsplash.com%2Fphoto-1454165804606-c3d57bc86b40%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDR8fHRlYWNoaW5nfGVufDB8fHx8MTc3MDI4NTUxN3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1454165804606-c3d57bc86b40%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDR8fHRlYWNoaW5nfGVufDB8fHx8MTc3MDI4NTUxN3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Who is going to train the juniors?" width="2000" height="1335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not long ago, junior engineers learned by working closely with more senior ones.&lt;/p&gt;

&lt;p&gt;Code reviews, design discussions, and mistakes created friction, and that friction built understanding.&lt;/p&gt;

&lt;p&gt;AI changed that.&lt;/p&gt;

&lt;p&gt;Today, answers are instant. Code is generated. Progress looks fast, but depth is optional. A junior can ship without ever fully understanding why something works. That’s powerful, and risky.&lt;/p&gt;

&lt;p&gt;During our last internship season, our intern chose not to use AI tools while coding. Short-term productivity suffered. Tasks took longer.&lt;/p&gt;

&lt;p&gt;But something more important happened: they struggled, debugged, and built intuition. When they would introduce AI tools, the effect would be dramatic. With experience in place, AI will become a multiplier instead of a shortcut. That’s the real distinction.&lt;/p&gt;

&lt;p&gt;AI is great at accelerating experienced engineers. It’s bad at replacing the process that creates them. If we remove friction too early, we don’t get better engineers, just faster output with hidden gaps. And those gaps surface later, when systems scale or fail.&lt;/p&gt;

&lt;p&gt;This isn’t about banning AI. It’s about being intentional. Let juniors struggle a bit. Accept short-term productivity loss.&lt;/p&gt;

&lt;p&gt;Otherwise, we’re not training the next generation of engineers; we’re just shipping code faster.&lt;/p&gt;

</description>
      <category>artificialintelligen</category>
      <category>thoughts</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How We Fixed SQLite Database Locks in Spring Boot (And Got a 5x Performance Boost)</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Fri, 30 Jan 2026 10:36:55 +0000</pubDate>
      <link>https://dev.to/rockandnull/how-we-fixed-sqlite-database-locks-in-spring-boot-and-got-a-5x-performance-boost-5nk</link>
      <guid>https://dev.to/rockandnull/how-we-fixed-sqlite-database-locks-in-spring-boot-and-got-a-5x-performance-boost-5nk</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%2Fimages.unsplash.com%2Fphoto-1614064641938-3bbee52942c7%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDN8fExvY2t8ZW58MHx8fHwxNzY5NzY2Mjg3fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1614064641938-3bbee52942c7%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDN8fExvY2t8ZW58MHx8fHwxNzY5NzY2Mjg3fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="How We Fixed SQLite Database Locks in Spring Boot (And Got a 5x Performance Boost)" width="2000" height="1334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We recently ran into a frustrating production issue: our Spring Boot application using SQLite would occasionally lock up, and the only way to recover was to restart the entire service. After some investigation and configuration changes, we not only fixed the locking issue but also saw dramatic performance improvements. Here's what we learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Our production SQLite database would intermittently enter a locked state, causing requests to hang and eventually fail. The database would remain locked until we performed a manual restart; not exactly the kind of reliability we wanted in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;SQLite is designed differently from traditional client-server databases like PostgreSQL or MySQL. It's a file-based database that works best with &lt;strong&gt;single-connection access&lt;/strong&gt;. However, Spring Boot's default connection pooling configuration doesn't account for SQLite's unique requirements, leading to connection conflicts and database locks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;We made several targeted configuration changes to our &lt;code&gt;application.properties&lt;/code&gt; to optimize Spring Boot for SQLite:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Single Connection Pool
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spring.datasource.hikari.maximum-pool-size=1
spring.datasource.hikari.minimum-idle=0

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

&lt;/div&gt;



&lt;p&gt;SQLite performs best with a single connection. Multiple connections can lead to locking issues, so we configured &lt;a href="https://github.com/brettwooldridge/HikariCP?ref=paleblueapps.com" rel="noopener noreferrer"&gt;HikariCP&lt;/a&gt; to maintain just one connection at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Aggressive Connection Management
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.idle-timeout=10000
spring.datasource.hikari.max-lifetime=30000

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

&lt;/div&gt;



&lt;p&gt;These settings ensure connections don't linger unnecessarily, reducing the chance of locks.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Leak Detection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spring.datasource.hikari.leak-detection-threshold=5000

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

&lt;/div&gt;



&lt;p&gt;This helps us spot any connections that are held longer than expected, making debugging much easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Disable Open-in-View
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spring.jpa.open-in-view=false

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

&lt;/div&gt;



&lt;p&gt;This was crucial. By default, Spring Boot keeps JPA connections open for the entire HTTP request lifecycle. We disabled this to ensure connections are released as soon as the database operation completes.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Manual Transaction Control
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spring.datasource.hikari.auto-commit=false

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

&lt;/div&gt;



&lt;p&gt;Disabling auto-commit gives us explicit control over transactions, preventing unexpected commits that could cause locking issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;The improvements were dramatic. We load-tested our API using k6 with identical test parameters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before the changes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total requests in 5 minutes: 1,736&lt;/li&gt;
&lt;li&gt;Success rate: 76%&lt;/li&gt;
&lt;li&gt;Failed requests: 411&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.amazonaws.com%2Fuploads%2Farticles%2F3bnjp4iuv5nz9vuxsx4d.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.amazonaws.com%2Fuploads%2Farticles%2F3bnjp4iuv5nz9vuxsx4d.png" alt="How We Fixed SQLite Database Locks in Spring Boot (And Got a 5x Performance Boost)" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After the changes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total requests in 5 minutes: 11,381&lt;/li&gt;
&lt;li&gt;Success rate: 99%&lt;/li&gt;
&lt;li&gt;Failed requests: 1&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.amazonaws.com%2Fuploads%2Farticles%2Fpcaqo7hwrnsmhia5mwr4.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.amazonaws.com%2Fuploads%2Farticles%2Fpcaqo7hwrnsmhia5mwr4.png" alt="How We Fixed SQLite Database Locks in Spring Boot (And Got a 5x Performance Boost)" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's a &lt;strong&gt;6.5x increase in throughput&lt;/strong&gt; and virtually eliminated failures. More importantly, we haven't seen a single database lock since deploying these changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SQLite isn't PostgreSQL&lt;/strong&gt; - Default Spring Boot configurations are optimized for client-server databases, not file-based ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single connection is key&lt;/strong&gt; - SQLite's architecture works best with one connection at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable open-in-view&lt;/strong&gt; - This JPA pattern is convenient but problematic with SQLite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor your connections&lt;/strong&gt; - Leak detection helped us identify issues during development.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're using SQLite with Spring Boot, these configurations can save you from production headaches and give you some nice performance wins along the way.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>sqlite</category>
    </item>
    <item>
      <title>Cog in a great machine VS building one from scratch</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Thu, 22 Jan 2026 15:41:08 +0000</pubDate>
      <link>https://dev.to/rockandnull/cog-in-a-great-machine-vs-building-one-from-scratch-4oi5</link>
      <guid>https://dev.to/rockandnull/cog-in-a-great-machine-vs-building-one-from-scratch-4oi5</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%2Fimages.unsplash.com%2Fphoto-1583198432859-635beb4e8600%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDN8fGNvZ3xlbnwwfHx8fDE3NjkwOTY0MTF8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1583198432859-635beb4e8600%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDN8fGNvZ3xlbnwwfHx8fDE3NjkwOTY0MTF8MA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Cog in a great machine VS building one from scratch" width="2000" height="1335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Having spent time as a software engineer in a large tech company and later launching my own company, I’m often asked which experience is “better.”&lt;br&gt;&lt;br&gt;
The honest answer is: they’re both exciting, deeply fulfilling in different ways, and not really comparable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Being a software engineer in a big company
&lt;/h2&gt;

&lt;p&gt;Working in a big company feels like being a cog in an incredible, well-oiled machine. And I don’t mean “cog” in a negative way.&lt;/p&gt;

&lt;p&gt;The machine is complex, powerful, and built by thousands of highly skilled people. Your role is clearly defined. You are expected to go deep, specialize, and deliver excellence within your scope. When something breaks, there’s a process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You identify the issue&lt;/li&gt;
&lt;li&gt;You report it&lt;/li&gt;
&lt;li&gt;It moves through product managers, designers, tech leads, and prioritization cycles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your responsibility often ends at &lt;em&gt;reporting clearly and correctly&lt;/em&gt;. Someone else owns the decision, someone else owns the fix, and that’s by design. This separation of concerns is what allows large organizations to scale.&lt;/p&gt;

&lt;p&gt;Something is reassuring about this. You can focus on engineering problems, grow technically, and learn from incredibly talented peers. The impact is massive, even if it’s sometimes indirect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Launching your own company
&lt;/h2&gt;

&lt;p&gt;Starting your own company feels very different.&lt;/p&gt;

&lt;p&gt;Instead of being a cog in a large machine, you’re building a much smaller machine from scratch. And at the beginning, you &lt;em&gt;are&lt;/em&gt; most of the parts.&lt;/p&gt;

&lt;p&gt;The boundaries between roles are blurry or nonexistent. You might notice a product issue, report it to yourself, prioritize it, design the solution, implement it, deploy it, and then answer the support ticket that comes in afterward.&lt;/p&gt;

&lt;p&gt;There’s no “this isn’t my job.”&lt;/p&gt;

&lt;p&gt;Even if your official role is “software engineer” or “founder,” reality doesn’t care. If something needs fixing and you’re the best person to do it, then that’s what you’ll do.&lt;/p&gt;

&lt;p&gt;This can be exhausting, but it’s also incredibly energizing. You see the full lifecycle of decisions. You feel the consequences immediately. The feedback loop is short and very real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two exciting, very different journeys
&lt;/h2&gt;

&lt;p&gt;What I’ve learned is that these two worlds shouldn’t be compared as better or worse.&lt;/p&gt;

&lt;p&gt;They’re simply different journeys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Big companies teach you scale, rigor, and depth&lt;/li&gt;
&lt;li&gt;Startups teach you ownership, adaptability, and breadth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One is about mastering your part of the system.&lt;br&gt;&lt;br&gt;
The other is about making sure the system works at all.&lt;/p&gt;

&lt;p&gt;Both are exciting. Both are challenging. And both shape you in ways that stay with you.&lt;/p&gt;

&lt;p&gt;The important thing isn’t choosing the “right” path, but understanding what kind of journey you’re on, and why you chose it.&lt;/p&gt;

</description>
      <category>thoughts</category>
    </item>
    <item>
      <title>Introducing Pale Blue Spring Admin: auto-generated admin UI for Spring Boot</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Fri, 14 Nov 2025 09:54:40 +0000</pubDate>
      <link>https://dev.to/rockandnull/introducing-pale-blue-spring-admin-auto-generated-admin-ui-for-spring-boot-1ccf</link>
      <guid>https://dev.to/rockandnull/introducing-pale-blue-spring-admin-auto-generated-admin-ui-for-spring-boot-1ccf</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%2Fimages.unsplash.com%2Fphoto-1525443205289-b4944046aa32%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDI0fHxjb250cm9sfGVufDB8fHx8MTc2Mjk5MDgxN3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1525443205289-b4944046aa32%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDI0fHxjb250cm9sfGVufDB8fHx8MTc2Mjk5MDgxN3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Introducing Pale Blue Spring Admin: auto-generated admin UI for Spring Boot" width="2000" height="1333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At Pale Blue, we’ve always admired the magic of &lt;strong&gt;Django Admin&lt;/strong&gt; - that moment when you define your models and instantly get a clean, intuitive interface to browse, search, and manage your data. When we transitioned our backend stack to &lt;strong&gt;Spring Boot&lt;/strong&gt; , we missed that simplicity — the ability to &lt;em&gt;see and interact&lt;/em&gt; with our data models without writing extra code or wiring up a custom UI.&lt;/p&gt;

&lt;p&gt;So… we built it.&lt;/p&gt;

&lt;p&gt;We’re excited to announce the release of our second open-source library: &lt;a href="https://github.com/PaleBlueApps/pale-blue-spring-admin?ref=paleblueapps.com" rel="noopener noreferrer"&gt;&lt;strong&gt;pale-blue-spring-admin&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pale Blue Spring Admin&lt;/strong&gt; brings an &lt;strong&gt;auto-generated admin interface&lt;/strong&gt; to your Spring Boot applications - inspired by Django’s admin, but built for the &lt;strong&gt;Spring + Kotlin&lt;/strong&gt; ecosystem.&lt;/p&gt;

&lt;p&gt;Once you install the library, it &lt;strong&gt;auto-discovers your JPA entities&lt;/strong&gt; and exposes them through an &lt;strong&gt;intuitive web interface&lt;/strong&gt;. From there, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browse all entities in your database&lt;/li&gt;
&lt;li&gt;View records with pagination, search, and basic sorting&lt;/li&gt;
&lt;li&gt;Navigate relationships through linked foreign keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Currently, the interface is &lt;strong&gt;read-only&lt;/strong&gt; , but we’re already working on &lt;strong&gt;write operations&lt;/strong&gt; (create, update, delete) for a future release.&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.amazonaws.com%2Fuploads%2Farticles%2Fqkgqumh56s3nt2zn8rt8.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.amazonaws.com%2Fuploads%2Farticles%2Fqkgqumh56s3nt2zn8rt8.png" alt="Introducing Pale Blue Spring Admin: auto-generated admin UI for Spring Boot" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Designed with simplicity in mind
&lt;/h3&gt;

&lt;p&gt;One of our core goals was simplicity. We wanted developers to drop the library into their Spring Boot project, define their JPA entities as usual, and immediately get a clean, intuitive interface - no boilerplate, no manual wiring, no custom views to maintain. The admin UI is intentionally minimal, predictable, and easy to navigate, making data exploration effortless even in large projects.&lt;/p&gt;

&lt;p&gt;Access to the admin interface must be explicitly defined, giving you full control over who can reach it and how they authenticate. In our documentation, we also provide a recommended lightweight authentication setup to help teams secure the admin area quickly, while still allowing more advanced configurations if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built in Kotlin. For Kotlin.
&lt;/h3&gt;

&lt;p&gt;This project is also a milestone for us - &lt;strong&gt;our first backend library written entirely in Kotlin&lt;/strong&gt; , marking our ongoing shift to Kotlin for backend development. We love the clarity and expressiveness Kotlin brings to Spring Boot projects, and we’re excited to contribute something useful back to the Kotlin and Spring communities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get Started
&lt;/h3&gt;

&lt;p&gt;The library is open-source and available now: &lt;a href="https://github.com/PaleBlueApps/pale-blue-spring-admin?ref=paleblueapps.com" rel="noopener noreferrer"&gt;github.com/PaleBlueApps/pale-blue-spring-admin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’d love your feedback, contributions, and ideas on how to make it even better.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>backend</category>
    </item>
    <item>
      <title>The rewrite dilemma in software engineering</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Thu, 06 Nov 2025 12:10:38 +0000</pubDate>
      <link>https://dev.to/rockandnull/the-rewrite-dilemma-in-software-engineering-446h</link>
      <guid>https://dev.to/rockandnull/the-rewrite-dilemma-in-software-engineering-446h</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%2Fimages.unsplash.com%2Fphoto-1677864234709-bde08838fb9d%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDR8fGxvb3B8ZW58MHx8fHwxNzYyNDMxMDEzfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1677864234709-bde08838fb9d%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDR8fGxvb3B8ZW58MHx8fHwxNzYyNDMxMDEzfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="The rewrite dilemma in software engineering" width="2000" height="1333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rewriting software is a topic that sparks endless debates among engineers. The first reaction when joining an existing project is often: “This is a mess, let’s start fresh”. It’s tempting to imagine a perfect, clean system - elegant architecture, fewer compromises, faster development.&lt;/p&gt;

&lt;p&gt;Sometimes a rewrite is justified. But what’s often overlooked are the small fixes, edge case patches, and tweaks accumulated over time. That “messy” system is often stable because of these hard-earned adjustments.&lt;/p&gt;

&lt;p&gt;A rewrite might feel faster initially, but regaining that stability - rediscovering subtle fixes, handling edge cases, and testing thoroughly - often takes longer than improving the existing code.&lt;/p&gt;

&lt;p&gt;The key is balance. Rewrites aren’t inherently bad, but they come with hidden costs. Stability, accumulated fixes, and historical knowledge are valuable, and any rewrite should account for them before lighting the match.&lt;/p&gt;

&lt;p&gt;Don’t chase perfection blindly - sometimes improving what works is smarter than starting over.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>thoughts</category>
    </item>
    <item>
      <title>T-Shaped engineers: the blueprint for building with AI</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Thu, 23 Oct 2025 14:11:21 +0000</pubDate>
      <link>https://dev.to/rockandnull/t-shaped-engineers-the-blueprint-for-building-with-ai-2d2a</link>
      <guid>https://dev.to/rockandnull/t-shaped-engineers-the-blueprint-for-building-with-ai-2d2a</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%2Fimages.unsplash.com%2Fphoto-1520641147456-f78b3e1d83b6%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDR8fFR8ZW58MHx8fHwxNzYxMjI4NDMyfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1520641147456-f78b3e1d83b6%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDR8fFR8ZW58MHx8fHwxNzYxMjI4NDMyfDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="T-Shaped engineers: the blueprint for building with AI" width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The concept of the T-shaped engineer isn't new. It's been around, quietly championed by forward-thinking tech leaders and organizations. But in 2025, as LLMs transform how we build software, this professional archetype has evolved from "nice to have" to essential.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes an Engineer T-Shaped?
&lt;/h2&gt;

&lt;p&gt;Picture the letter T. The horizontal bar represents breadth - a working knowledge across a wide range of technologies, methodologies, and domains. The vertical stem represents depth - expert-level mastery in one or a few specific areas.&lt;/p&gt;

&lt;p&gt;A T-shaped engineer might have deep expertise in distributed systems architecture while maintaining conversational fluency in frontend frameworks, DevOps practices, data engineering, and security principles. They can collaborate meaningfully with specialists across disciplines because they understand the fundamentals, even if they're not experts in everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Amplification Effect
&lt;/h2&gt;

&lt;p&gt;Here's where the AI era changes everything.&lt;/p&gt;

&lt;p&gt;With LLMs, both general SOTA models and countless domain-specific models, the barrier to working across multiple technologies has dropped dramatically. An engineer with fundamental understanding can now leverage AI to scaffold applications in languages they haven't touched in years, generate infrastructure-as-code configurations, or even implement features in unfamiliar frameworks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The breadth of the T becomes supercharged.&lt;/strong&gt;  That conversational knowledge of various domains? It's now enough to effectively direct AI tools to produce working solutions across technologies you don't use daily.&lt;/p&gt;

&lt;p&gt;But here's the critical insight:  &lt;strong&gt;AI is a powerful accelerator, but it's not infallible.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Depth as the Safety Net
&lt;/h2&gt;

&lt;p&gt;This is where the depth of the T becomes your organization's safety net and quality standard.&lt;/p&gt;

&lt;p&gt;In your areas of deep expertise, you can catch AI hallucinations and incorrect implementations before they reach production, guide AI tools toward best practices that go beyond surface-level correctness, make architectural decisions that AI can't reliably make on its own, identify security vulnerabilities or performance issues that AI-generated code might introduce, and maintain high standards even when moving fast with AI assistance.&lt;/p&gt;

&lt;p&gt;Think of it this way: AI gives you the ability to paint across a massive canvas. Your depth expertise ensures that the critical parts of that painting are museum-quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Growing T-Shaped Engineers
&lt;/h2&gt;

&lt;p&gt;Becoming T-shaped isn't about forcing engineers to master everything. It's about recognizing and developing depth in areas where engineers show natural aptitude and interest, encouraging curiosity across domains without demanding mastery, creating opportunities for meaningful exposure to different parts of the stack, and celebrating both deep expertise and collaborative breadth.&lt;/p&gt;

&lt;p&gt;The engineers who will define the next decade aren't those who can do everything or those who know only one thing. They're the ones who can go deep where it matters and go wide where AI can help - combining human expertise with artificial intelligence to build better software, faster and more safely than either could alone.&lt;/p&gt;

&lt;p&gt;The T-shaped engineer isn't just relevant in the AI era. It's the blueprint for how exceptional engineers will work.&lt;/p&gt;

</description>
      <category>thoughts</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Automatic resource cleanup in Jetpack ViewModels using AutoCloseable</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Tue, 07 Oct 2025 09:15:15 +0000</pubDate>
      <link>https://dev.to/rockandnull/automatic-resource-cleanup-in-jetpack-viewmodels-using-autocloseable-kbb</link>
      <guid>https://dev.to/rockandnull/automatic-resource-cleanup-in-jetpack-viewmodels-using-autocloseable-kbb</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%2Fimages.unsplash.com%2Fphoto-1563453392212-326f5e854473%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDJ8fGNsZWFuZXJ8ZW58MHx8fHwxNzU5ODI3NTU5fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1563453392212-326f5e854473%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDJ8fGNsZWFuZXJ8ZW58MHx8fHwxNzU5ODI3NTU5fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Automatic resource cleanup in Jetpack ViewModels using AutoCloseable" width="2000" height="1333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When building Android applications with Jetpack ViewModels, proper resource management is crucial to prevent memory leaks and ensure efficient cleanup of services. In this post, we’ll explore an elegant pattern that leverages Kotlin’s &lt;strong&gt;AutoCloseable&lt;/strong&gt; interface to clean up resources when a ViewModel is destroyed automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Traditionally, when a ViewModel needs to clean up resources, we override the &lt;code&gt;onCleared()&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class MyViewModel(private val itemsService: ItemsService) : ViewModel() {

    override fun onCleared() {
        super.onCleared()
        itemsService.cleanup()
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this works, it requires boilerplate code for each service that needs cleanup.&lt;/p&gt;

&lt;h3&gt;
  
  
  AutoCloseable Pattern
&lt;/h3&gt;

&lt;p&gt;Android’s &lt;code&gt;ViewModel&lt;/code&gt; class supports automatic resource cleanup through the &lt;code&gt;AutoCloseable&lt;/code&gt; interface. When a ViewModel is cleared, any services that implement &lt;code&gt;AutoCloseable&lt;/code&gt; and are passed to the ViewModel constructor will automatically have their &lt;code&gt;close()&lt;/code&gt; method invoked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing AutoCloseable Services
&lt;/h3&gt;

&lt;p&gt;First, make your service implement &lt;code&gt;AutoCloseable&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface ItemsService : AutoCloseable {
    val items: Flow&amp;lt;List&amp;lt;Item&amp;gt;&amp;gt;
    suspend fun getItems(): List&amp;lt;Item&amp;gt;
    // ... other methods
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then implement the &lt;code&gt;close()&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class RealItemsService : ItemsService {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    [....]

    override fun close() {
        scope.cancel()
        println("ItemsService closed - resources cleaned up")
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Passing AutoCloseable Services to a ViewModel
&lt;/h3&gt;

&lt;p&gt;Here’s the key: you can directly pass all your &lt;code&gt;AutoCloseable&lt;/code&gt; services to the &lt;code&gt;ViewModel&lt;/code&gt; constructor, and let the framework handle cleanup automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class HomeViewModel(
    private val itemsService: ItemsService,
    private val customersService: CustomersService,
    private val invoiceService: InvoiceService
) : ViewModel(
    itemsService,
    customersService,
    invoiceService
) {

    [....]    
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;HomeViewModel&lt;/code&gt; is cleared, the Android framework automatically calls &lt;code&gt;close()&lt;/code&gt; on each of these services — no need to override &lt;code&gt;onCleared()&lt;/code&gt; or introduce a custom &lt;code&gt;BaseViewModel&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;AutoCloseable&lt;/code&gt; pattern in ViewModels provides an elegant solution for automatic resource management. By implementing &lt;code&gt;AutoCloseable&lt;/code&gt; on your services and passing them to the ViewModel constructor, you ensure proper cleanup without writing repetitive boilerplate code. This approach leads to cleaner, more maintainable code while preventing memory leaks and ensuring resources are released appropriately.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>android</category>
      <category>jetpackcompose</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Billin: Building a modern cross-platform invoice app with Compose Multiplatform</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Mon, 22 Sep 2025 13:30:53 +0000</pubDate>
      <link>https://dev.to/rockandnull/billin-building-a-modern-cross-platform-invoice-app-with-compose-multiplatform-pm9</link>
      <guid>https://dev.to/rockandnull/billin-building-a-modern-cross-platform-invoice-app-with-compose-multiplatform-pm9</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%2Fimages.unsplash.com%2Fphoto-1516491974194-9aa6d0b04d1a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDU2fHxtb2Rlcm58ZW58MHx8fHwxNzU4MjA4NDQ0fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1516491974194-9aa6d0b04d1a%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDU2fHxtb2Rlcm58ZW58MHx8fHwxNzU4MjA4NDQ0fDA%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Billin: Building a modern cross-platform invoice app with Compose Multiplatform" width="2000" height="1456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When we set out to build &lt;a href="https://www.getbillin.com/?ref=paleblueapps.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Billin&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;-&lt;/strong&gt; our smart invoice maker for freelancers, small businesses, and professionals - we knew we wanted to deliver a clean, reliable experience that helps people &lt;strong&gt;create, send, and track professional invoices in seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On Android, this was straightforward: Kotlin and Jetpack Compose gave us the productivity we needed, and our users loved the fast, polished results. But we didn’t want to stop there. Freelancers and small businesses often work across devices and ecosystems, and many of them asked for an iOS version. That’s where &lt;a href="https://www.jetbrains.com/compose-multiplatform/?ref=paleblueapps.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Compose Multiplatform (CMP)&lt;/strong&gt;&lt;/a&gt; came into play.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Compose Multiplatform?
&lt;/h2&gt;

&lt;p&gt;With Billin, users can preview invoices as PDFs, track payments, manage clients, and manage their services inventory - all from their phone. To bring this experience to iOS, we had two choices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a completely separate SwiftUI app.&lt;/li&gt;
&lt;li&gt;Leverage the tools we already loved (Kotlin + Compose) and extend them to iOS.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We chose option two - and &lt;a href="https://www.jetbrains.com/compose-multiplatform/?ref=paleblueapps.com" rel="noopener noreferrer"&gt;Compose Multiplatform (CMP)&lt;/a&gt; proved to be the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and solutions
&lt;/h2&gt;

&lt;p&gt;Migrating from Android-first to CMP came with a few bumps. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On Android, we relied on the &lt;strong&gt;AndroidX PDF Preview component&lt;/strong&gt; to display invoices. On iOS, this wasn’t available. Our solution was to &lt;strong&gt;generate image previews&lt;/strong&gt; of invoices, which worked flawlessly across both platforms.&lt;/li&gt;
&lt;li&gt;Some UI conventions simply didn’t translate well. We &lt;strong&gt;disabled ripple effects&lt;/strong&gt; on iOS and replaced certain screen transitions with animations that feel more at home in the Apple ecosystem.&lt;/li&gt;
&lt;li&gt;We carefully reworked layouts and spacing so that iOS users wouldn’t feel like they were using a “ported” Android app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The good news? Most of the Jetpack Compose libraries we were already using had multiplatform support. And with &lt;a href="https://dev.to/mavris/streamline-your-kmp-development-with-pale-blue-kmp-core-2jaa-temp-slug-5887649"&gt;our own open-source library&lt;/a&gt;, &lt;a href="https://github.com/PaleBlueApps/pale-blue-kmp-core?ref=paleblueapps.com" rel="noopener noreferrer"&gt;&lt;code&gt;pale-blue-kmp-core&lt;/code&gt;&lt;/a&gt;, we had a big head start in sharing logic across platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture behind Billin
&lt;/h2&gt;

&lt;p&gt;Billin is built on a &lt;strong&gt;service-based architecture&lt;/strong&gt; , which has been key in making multiplatform development efficient. Here’s how:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We use a &lt;code&gt;SystemService&lt;/code&gt; layer to abstract system-level interactions (like file storage or sharing).&lt;/li&gt;
&lt;li&gt;With Kotlin’s &lt;strong&gt;&lt;code&gt;expect/actual&lt;/code&gt;&lt;/strong&gt; mechanism, we implement platform-specific code while keeping most of the logic shared.&lt;/li&gt;
&lt;li&gt;Our &lt;strong&gt;monorepo pattern&lt;/strong&gt; allows us to share models, business logic, and even some backend code between Android, iOS, and our server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Speaking of the server, our backend is also written in &lt;strong&gt;Kotlin (Spring)&lt;/strong&gt;. That means Billin isn’t just multiplatform on mobile - it’s part of a unified Kotlin ecosystem that runs across frontend, backend, and shared libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivering a cross-platform design that feels right
&lt;/h2&gt;

&lt;p&gt;For Billin, we made a deliberate design choice: the app should look &lt;strong&gt;clean and modern&lt;/strong&gt; , but not be tightly coupled to either Android’s Material Design or iOS’s Human Interface Guidelines. Instead, we aimed for a &lt;strong&gt;neutral yet elegant visual style&lt;/strong&gt; that feels familiar on both platforms.&lt;/p&gt;

&lt;p&gt;That said, we didn’t ignore platform conventions. We still rely on &lt;strong&gt;native UI elements and animations&lt;/strong&gt; where they make sense—for example, transitions on iOS behave like iOS users expect, and system dialogs follow platform guidelines. By combining a shared, platform-agnostic design language with native touches, Billin feels &lt;strong&gt;consistent and professional&lt;/strong&gt; while still feeling at home on whichever device you’re using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simplifying In-App Purchases with RevenueCat
&lt;/h2&gt;

&lt;p&gt;One area where we were particularly impressed was &lt;strong&gt;in-app purchases&lt;/strong&gt;. Setting up IAPs in the Play Console and App Store Connect is still required, but managing them across both platforms can be a real pain - keeping product IDs consistent, making sure pricing aligns, and then wiring everything up in the client. &lt;a href="https://www.revenuecat.com/?ref=paleblueapps.com" rel="noopener noreferrer"&gt;RevenueCat&lt;/a&gt; solved much of that complexity by providing a &lt;strong&gt;dynamic, configurable paywall&lt;/strong&gt; that pulls in the right products at runtime.&lt;/p&gt;

&lt;p&gt;This not only meant we didn’t have to hard-code product lists, but also gave us the ability to &lt;strong&gt;A/B test marketing messages and pricing strategies&lt;/strong&gt; with just a few clicks. On top of that, the unified reporting across platforms makes it far easier to get a single picture of subscriptions and revenue. And the fact that RevenueCat ships with a &lt;a href="https://www.revenuecat.com/docs/getting-started/installation/kotlin-multiplatform?ref=paleblueapps.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Kotlin Multiplatform (KMP) SDK&lt;/strong&gt;&lt;/a&gt; was a huge bonus - integrating it into our existing architecture was smooth and developer-friendly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Billin offers
&lt;/h2&gt;

&lt;p&gt;At its core, Billin is an invoicing tool designed to help freelancers and small businesses manage their billing process on mobile. The app supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invoice creation&lt;/strong&gt; – generate invoices with support for taxes, discounts, and automatic totals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracking and Reporting&lt;/strong&gt; – keep a record of payments, overdue items, expenses, and client summaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entities Management&lt;/strong&gt; – maintain items and services you offer for quick invoice creation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-Platform Access&lt;/strong&gt; – preview invoices as PDFs or images, share them securely, and access data from multiple devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This functionality is wrapped in a design that is clean and modern, intentionally not tied too closely to either Android or iOS design systems, while still using native UI elements and animations to feel natural on each platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The power of CMP
&lt;/h2&gt;

&lt;p&gt;For us, Compose Multiplatform (CMP) wasn't just about writing less code. They were about creating a &lt;strong&gt;seamless cross-platform experience&lt;/strong&gt; while keeping our development process lean and scalable.&lt;/p&gt;

&lt;p&gt;Billin today is both a &lt;strong&gt;native Android app&lt;/strong&gt; and a &lt;strong&gt;wonderful iOS app&lt;/strong&gt; , built from the same shared foundation. With thoughtful adjustments, it feels fully integrated into each ecosystem while maintaining a consistent experience across platforms. Combined with our Kotlin/Spring backend and monorepo approach, we’ve created a unified stack where models, logic, and services can be reused everywhere.&lt;/p&gt;

&lt;p&gt;This approach has allowed us to move faster, reduce duplication, and deliver apps that feel at home on both &lt;a href="https://play.google.com/store/apps/details?id=com.paleblueapps.billin&amp;amp;ref=paleblueapps.com" rel="noopener noreferrer"&gt;Android&lt;/a&gt; and iOS while staying true to a clean, platform-agnostic design.&lt;/p&gt;

</description>
      <category>composemultiplatform</category>
      <category>android</category>
      <category>ios</category>
      <category>jetpackcompose</category>
    </item>
    <item>
      <title>Navigate back with results in Jetpack Compose Navigation</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Fri, 19 Sep 2025 08:57:31 +0000</pubDate>
      <link>https://dev.to/rockandnull/navigate-back-with-results-in-jetpack-compose-navigation-46hn</link>
      <guid>https://dev.to/rockandnull/navigate-back-with-results-in-jetpack-compose-navigation-46hn</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%2Fimages.unsplash.com%2Fphoto-1470645792662-dd18394f8c97%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDJ8fG5hdmlnYXRlfGVufDB8fHx8MTc1ODIwMjcwM3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1470645792662-dd18394f8c97%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDJ8fG5hdmlnYXRlfGVufDB8fHx8MTc1ODIwMjcwM3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Navigate back with results in Jetpack Compose Navigation" width="2000" height="1335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’ve ever needed to open a screen, let the user pick something, and then pass that selection back to the previous screen, you probably reached for shared ViewModels, singletons, or brittle route parameters.&lt;/p&gt;

&lt;p&gt;There’s a simpler way that works with Jetpack Compose Navigation: navigate forward → wait for a result → navigate back with that result.&lt;/p&gt;

&lt;p&gt;This post introduces a tiny helper you can drop into your project and explains how it works, complete with examples, pitfalls, and testing tips.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Helper
&lt;/h2&gt;

&lt;p&gt;Put this file somewhere in your project as-is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.json.Json

private const val RESULT_KEY = "result"

fun &amp;lt;T&amp;gt; NavController.navigateBackWithResult(value: T) {
    previousBackStackEntry?.savedStateHandle?.set(RESULT_KEY, value)
    navigateUp()
}

inline fun &amp;lt;reified T&amp;gt; NavController.navigateBackWithSerializableResult(value: T) {
    navigateBackWithResult(Json.encodeToString(value))
}

suspend fun &amp;lt;T&amp;gt; NavController.navigateForResult(route: Any): T? =
    suspendCancellableCoroutine { continuation -&amp;gt;
        val currentNavEntry = currentBackStackEntry
            ?: throw IllegalStateException("No current back stack entry found")

        navigate(route)

        val lifecycleObserver = object : LifecycleEventObserver {
            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
                if (event == Lifecycle.Event.ON_START) {
                    continuation.resume(currentNavEntry.savedStateHandle[RESULT_KEY])
                    currentNavEntry.savedStateHandle.remove&amp;lt;T&amp;gt;(RESULT_KEY)
                    currentNavEntry.lifecycle.removeObserver(this)
                }
            }
        }

        currentNavEntry.lifecycle.addObserver(lifecycleObserver)

        continuation.invokeOnCancellation {
            currentNavEntry.savedStateHandle.remove&amp;lt;T&amp;gt;(RESULT_KEY)
            currentNavEntry.lifecycle.removeObserver(lifecycleObserver)
        }
    }

suspend inline fun &amp;lt;reified T&amp;gt; NavController.navigateForSerializableResult(route: Any): T? {
    val result: String = navigateForResult(route) ?: return null
    return Json.decodeFromString(result)
}

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

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;NavigateBackWithResult.kt&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Caller launches&lt;/strong&gt; a route with &lt;code&gt;navigateForResult(route)&lt;/code&gt;.
The current entry is remembered, navigation occurs, and a lifecycle observer is set up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Callee finishes&lt;/strong&gt; by calling &lt;code&gt;navigateBackWithResult(value)&lt;/code&gt;.
The result is stored in the caller’s &lt;code&gt;SavedStateHandle&lt;/code&gt; under a fixed key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caller resumes&lt;/strong&gt; on &lt;code&gt;ON_START&lt;/code&gt;.
The coroutine unblocks, retrieves the result, and clears the key to avoid stale values.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is &lt;strong&gt;lifecycle-aware, type-safe, and minimal&lt;/strong&gt; - no &lt;code&gt;ActivityResultContract&lt;/code&gt;, no shared &lt;code&gt;ViewModel&lt;/code&gt;, no event bus.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to use it
&lt;/h3&gt;

&lt;p&gt;Here's a simple example to see the pattern in action. Imagine a screen where the user picks a color, and when they go back, the chosen value is passed to the previous screen. This is the most straightforward case: a basic type (&lt;code&gt;String&lt;/code&gt;) being sent back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Routes:

object Routes {
    const val HOME = "home"
    const val COLOR_PICKER = "color-picker"
}

// Nav host:

@Composable
fun AppNavHost(navController: NavHostController = rememberNavController()) {
    NavHost(navController, startDestination = Routes.HOME) {
        composable(Routes.HOME) { HomeScreen(navController) }
        composable(Routes.COLOR_PICKER) { ColorPickerScreen(navController) }
    }
}

// Caller (HomeScreen):

@Composable
fun HomeScreen(navController: NavController) {
    val scope = rememberCoroutineScope()
    var selectedColor by remember { mutableStateOf&amp;lt;String?&amp;gt;(null) }

    Column(Modifier.padding(16.dp)) {
        Text("Selected: ${'$'}{selectedColor ?: "none"}")
        Button(onClick = {
            scope.launch {
                val result: String? = navController.navigateForResult(Routes.COLOR_PICKER)
                selectedColor = result
            }
        }) {
            Text("Pick color")
        }
    }
}

// Callee (ColorPickerScreen):

@Composable
fun ColorPickerScreen(navController: NavController) {
    val colors = listOf("Red", "Green", "Blue")
    Column(Modifier.padding(16.dp)) {
        colors.forEach { color -&amp;gt;
            Button(onClick = { navController.navigateBackWithResult(color) }) {
                Text(color)
            }
        }
        OutlinedButton(onClick = { navController.navigateUp() }) {
            Text("Cancel")
        }
    }
}

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Edge cases and gotchas
&lt;/h3&gt;

&lt;p&gt;There are a few edge cases and nuances worth keeping in mind. Results are always nullable (&lt;code&gt;T?&lt;/code&gt;), so you should be prepared to handle the case where the user cancels or navigates back without setting anything. If you use the non-serializable variant and the caller and callee don’t agree on the type, you’ll hit a &lt;code&gt;ClassCastException&lt;/code&gt;, so type alignment is critical. Results also don’t survive process death, since this mechanism is essentially a callback rather than persistent state. Finally, avoid waiting for multiple results concurrently from the same destination; if you need that, wrap access in your own guard or queue to prevent conflicts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;This helper gives you a lightweight, lifecycle-aware pattern for passing data back through the Compose Navigation stack: you simply drop it into your project, call &lt;code&gt;navigateForResult()&lt;/code&gt; when moving forward, and then call &lt;code&gt;navigateBackWithResult()&lt;/code&gt; when returning. That’s all it takes - no extra contracts and no brittle hacks.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>android</category>
      <category>jetpackcompose</category>
    </item>
    <item>
      <title>AI everywhere: The hype, the misfits, and the inevitable maturity</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Thu, 04 Sep 2025 13:40:30 +0000</pubDate>
      <link>https://dev.to/rockandnull/ai-everywhere-the-hype-the-misfits-and-the-inevitable-maturity-4hjl</link>
      <guid>https://dev.to/rockandnull/ai-everywhere-the-hype-the-misfits-and-the-inevitable-maturity-4hjl</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%2Fimages.unsplash.com%2Fphoto-1542062700-9b61ccbc1696%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fHRyZW5kfGVufDB8fHx8MTc1Njk5Mjk3N3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1542062700-9b61ccbc1696%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDF8fHRyZW5kfGVufDB8fHx8MTc1Njk5Mjk3N3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="AI everywhere: The hype, the misfits, and the inevitable maturity" width="2000" height="1333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Artificial Intelligence is here to stay. But right now, it’s being added to everything - whether it’s a good fit or not.&lt;/p&gt;

&lt;p&gt;I’ve seen this before. When mobile was the big trend, businesses rushed to build apps simply because everyone else was. Most of those apps added no value and quickly faded. When crypto was hot, simple systems that could have been handled by a database suddenly had “blockchain” bolted onto them - just because it sounded innovative.&lt;/p&gt;

&lt;p&gt;AI is going through the same phase. A recent &lt;a href="https://hbr.org/2025/08/beware-the-ai-experimentation-trap?ref=paleblueapps.com" rel="noopener noreferrer"&gt;MIT report&lt;/a&gt; noted that 95% of AI investments generate zero return, usually because they’re solving problems that don’t need AI in the first place. The result is wasted time, wasted money, and features no one uses.&lt;/p&gt;

&lt;p&gt;But this cycle is normal. Technologies go from hype → misapplication → maturity. Over time, the buzzwords disappear, and the tech just becomes infrastructure. Nobody says “we use machine learning” when their photos are tagged automatically - they just say “face recognition.” AI will follow the same path.&lt;/p&gt;

&lt;p&gt;As the saying goes, "AI is whatever hasn't been done yet". Once it works, it’s just part of the product.&lt;/p&gt;

&lt;p&gt;Focus on solving real problems, and the right technology - AI or not - will find its place.&lt;/p&gt;

</description>
      <category>thoughts</category>
      <category>artificialintelligen</category>
    </item>
    <item>
      <title>Big clients aren’t always a big win</title>
      <dc:creator>RockAndNull</dc:creator>
      <pubDate>Thu, 24 Jul 2025 14:14:16 +0000</pubDate>
      <link>https://dev.to/rockandnull/big-clients-arent-always-a-big-win-3bho</link>
      <guid>https://dev.to/rockandnull/big-clients-arent-always-a-big-win-3bho</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%2Fimages.unsplash.com%2Fphoto-1568430328012-21ed450453ea%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDV8fHdoYWxlfGVufDB8fHx8MTc1MzM2NjIyM3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" 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%2Fimages.unsplash.com%2Fphoto-1568430328012-21ed450453ea%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixid%3DM3wxMTc3M3wwfDF8c2VhcmNofDV8fHdoYWxlfGVufDB8fHx8MTc1MzM2NjIyM3ww%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D2000" alt="Big clients aren’t always a big win" width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At first, it feels great. A big client, the kind you might call a “whale”, signs on. Revenue spikes, your roadmap gets direction, and there’s a sense of security. After all, what could go wrong with landing a massive account?&lt;/p&gt;

&lt;p&gt;But let’s be honest: “whales” aren’t always the win they seem to be. Sometimes, they’re a trap.&lt;/p&gt;

&lt;p&gt;We’ve seen it firsthand and talked about it again recently — mostly as a reminder to ourselves. When a single client becomes too important to your business, you're not scaling — you're becoming dependent. And dependency is the opposite of stability.&lt;/p&gt;

&lt;p&gt;When one client controls too much of your revenue, they also start to control your priorities. They might dictate timelines, influence your product direction, and make demands that smaller clients would never dare. Suddenly, you're no longer building your own company — you're building theirs.&lt;/p&gt;

&lt;p&gt;And if that whale ever swims away? You’re in deep water.&lt;/p&gt;

&lt;p&gt;There’s more long-term health in a diverse client base, a balanced ecosystem with many smaller (but consistent) clients. It might not feel as glamorous, but it’s sustainable. You gain resilience. You avoid single points of failure. You keep your team and your product focused on a broader vision, not the whims of one massive account.&lt;/p&gt;

&lt;p&gt;This isn’t to say big clients are bad. They can be incredible partners when the relationship is healthy and well-bounded. But the moment a whale starts to define your business, it’s worth taking a step back.&lt;/p&gt;

&lt;p&gt;So if you’ve landed one — great. Celebrate the win. But also, build your raft.&lt;/p&gt;

</description>
      <category>business</category>
    </item>
  </channel>
</rss>
