<?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: Aditya Pradhan</title>
    <description>The latest articles on DEV Community by Aditya Pradhan (@adityapradhan10).</description>
    <link>https://dev.to/adityapradhan10</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%2F637618%2F9c558f5a-1952-443c-b196-fa082c0f8f0b.png</url>
      <title>DEV Community: Aditya Pradhan</title>
      <link>https://dev.to/adityapradhan10</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adityapradhan10"/>
    <language>en</language>
    <item>
      <title>Database per Service: ownership, not isolation</title>
      <dc:creator>Aditya Pradhan</dc:creator>
      <pubDate>Wed, 10 Jun 2026 18:42:46 +0000</pubDate>
      <link>https://dev.to/adityapradhan10/database-per-service-ownership-not-isolation-3ad6</link>
      <guid>https://dev.to/adityapradhan10/database-per-service-ownership-not-isolation-3ad6</guid>
      <description>&lt;p&gt;Every microservices deck draws the same box: one database per service, labeled as a security boundary. That label is the assumption most teams copy. Database per Service pays off for a different reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The coupling you feel before the outage
&lt;/h2&gt;

&lt;p&gt;Picture two product teams on one Postgres instance. Team A ships a column rename Friday afternoon. Team B's release does not go out until Monday because migration ordering became a meeting, not a merge.&lt;/p&gt;

&lt;p&gt;Nobody got paged. The deploy queue stalled. That is what shared-schema coupling looks like in week two of "we are microservices now."&lt;/p&gt;

&lt;p&gt;For a long time one database with many services was normal. Coordinated migration windows, shared release trains, one schema everyone negotiated. The diagram said microservices. The datastore said monolith.&lt;/p&gt;

&lt;p&gt;Then the org splits into teams with different roadmaps. Same database. Different release trains. The pain shows up in deploy queues long before it shows up in an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Database per Service actually decouples
&lt;/h2&gt;

&lt;p&gt;Database per Service is not isolation theater. It separates three things a shared database fuses together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Schema ownership&lt;/strong&gt; — who can change the tables, and who gets called when a migration fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment cadence&lt;/strong&gt; — how fast each team can ship schema changes without a committee.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure blast radius&lt;/strong&gt; — how far a poison migration or lock contention fans out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each datastore is a bounded context contract. You are not drawing a security perimeter on a slide. You are deciding who can change &lt;code&gt;orders.status&lt;/code&gt; without a three-team approval thread.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph shared["Shared database (coupled)"]
    S1[Orders service] --&amp;gt; PG[(Postgres)]
    S2[Inventory service] --&amp;gt; PG
    S3[Billing service] --&amp;gt; PG
  end

  subgraph split["Database per Service (decoupled)"]
    O[Orders] --&amp;gt; DO[(orders_db)]
    I[Inventory] --&amp;gt; DI[(inventory_db)]
    B[Billing] --&amp;gt; DB[(billing_db)]
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What you trade away
&lt;/h2&gt;

&lt;p&gt;Independence has a price. The weekly revenue report that used to be one query becomes a pipeline problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Works on a shared schema. Breaks when orders and inventory are separate databases.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the split, that join lives somewhere else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An outbox stream into a warehouse.&lt;/li&gt;
&lt;li&gt;A materialized view fed by CDC.&lt;/li&gt;
&lt;li&gt;A saga with compensating steps instead of a cross-service transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those is simpler than a join. They are simpler than four teams negotiating one migration order every sprint.&lt;/p&gt;

&lt;p&gt;Cadence is the second win. Billing ships schema v3 while catalog stays on v1.&lt;/p&gt;

&lt;p&gt;Blast radius is the third. A poison migration in one database does not lock tables in another.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to split vs when to stay shared
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Split databases&lt;/strong&gt; when team boundaries and release independence matter more than cross-service transactions and ad-hoc reporting joins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep one shared database&lt;/strong&gt; when you are still one product, one release train, and analytics lives on relational joins you are not ready to rebuild as events.&lt;/p&gt;

&lt;p&gt;If you copied the diagram but not the org structure, you pay the distributed-data tax without getting the ownership benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision checklist
&lt;/h2&gt;

&lt;p&gt;Before you draw separate database boxes on the next architecture review, ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Do different teams own different services with different roadmaps?&lt;/li&gt;
&lt;li&gt;Has a shared migration already blocked someone's deploy?&lt;/li&gt;
&lt;li&gt;Would a bad migration in Service A be acceptable if it took down Service B's tables?&lt;/li&gt;
&lt;li&gt;Is your reporting workload built on cross-service SQL joins you cannot yet replace with events or a warehouse?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If 1 and 2 are yes and 3 is no, Database per Service is probably earning its keep. If you are still one team shipping one product, a shared database is often the honest choice until the org boundary catches up to the diagram.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>microservices</category>
      <category>architecture</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Implementing Passkeys Beyond the Demo</title>
      <dc:creator>Aditya Pradhan</dc:creator>
      <pubDate>Wed, 17 Dec 2025 09:24:40 +0000</pubDate>
      <link>https://dev.to/adityapradhan10/implementing-passkeys-beyond-the-demo-124k</link>
      <guid>https://dev.to/adityapradhan10/implementing-passkeys-beyond-the-demo-124k</guid>
      <description>&lt;p&gt;We did not introduce passkeys to replace passwords. Passwords are still very much part of our system, especially as a fallback and for account recovery. The real problem we were trying to solve was repeated login friction.&lt;/p&gt;

&lt;p&gt;For product reasons, we do not keep users logged in when the browser is closed. That meant even active users were entering their passwords far more often than they would expect. Over time, this showed up as friction rather than outright failure. People were not locked out, but they were annoyed. Login felt like work, especially on shared devices or short sessions.&lt;/p&gt;

&lt;p&gt;Passkeys stood out because they reduced that friction without changing our core security posture. There was no need to introduce long-lived sessions or relax existing constraints. A biometric or platform authenticator prompt was faster than typing a password, even if it happened often. The goal was not passwordless accounts, but smoother re-authentication.&lt;/p&gt;

&lt;p&gt;At a high level, passkeys are a way to authenticate users using device-bound credentials backed by public key cryptography. From a user perspective, they feel closer to unlocking a device than logging into a website. That distinction mattered more than the underlying standard.&lt;/p&gt;

&lt;p&gt;What followed was less about implementing WebAuthn itself and more about fitting a new login primitive into a system that already had assumptions baked into it. Passkeys worked well in isolation. Making them coexist cleanly with passwords, recovery flows, and real user behaviour was where most of the complexity lived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Passkeys at a High Level
&lt;/h2&gt;

&lt;p&gt;At a conceptual level, passkeys are not complicated. Instead of proving identity by sending a shared secret like a password to the server, the browser uses a key pair that was created earlier and is tied to the user and the device. The private key never leaves the device. The server only stores a public key and asks the client to prove possession when needed.&lt;/p&gt;

&lt;p&gt;From an implementation point of view, WebAuthn gives you two main flows. One for creating a credential and one for asserting it later during login. The browser mediates both, and the user experience is largely controlled by the platform, not your UI. That is both a strength and a constraint.&lt;/p&gt;

&lt;p&gt;Passkeys are device-bound by default, sometimes synced across devices depending on the platform, and they depend heavily on browser and OS behaviour that your application does not control. That has implications for UX, support, and how you reason about account access.&lt;/p&gt;

&lt;p&gt;Once we got past the conceptual simplicity, the gaps started to show. The demo flows worked fine. The real system, with real users and real constraints, was where things became more interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Was Not the Hard Part
&lt;/h2&gt;

&lt;p&gt;Once we committed to passkeys, the core implementation moved faster than expected. There are enough reference implementations available now to avoid guessing, and we leaned on Google’s example repository to validate the overall flow and API boundaries.&lt;/p&gt;

&lt;p&gt;We also chose early on not to touch protocol-level details ourselves. Using &lt;code&gt;simplewebauthn&lt;/code&gt; meant binary conversions, base64 handling, and request shaping were taken care of. That kept the passkey code small and isolated, and reduced the risk of subtle bugs that are hard to reason about later.&lt;/p&gt;

&lt;p&gt;From a pure coding perspective, the passkey flows were simpler than our existing password flows. Fewer UI states and fewer opportunities for partial failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reality Hit During Cross-Device Testing
&lt;/h2&gt;

&lt;p&gt;The first real friction showed up when we tried to build confidence beyond a single setup. Passkeys are device-bound, which sounds manageable until you start testing across Macs, Windows machines, iPhones, and Android devices.&lt;/p&gt;

&lt;p&gt;Browser differences added another layer. Chrome and Firefox did not always behave the same way on the same hardware. Mobile browsers introduced their own quirks around prompts and interruptions. Some combinations worked reliably, others failed intermittently, and not all failures were easy to reproduce.&lt;/p&gt;

&lt;p&gt;Automated tests helped at the API layer, but they barely touched the actual user experience. Most confidence came from manual testing on real devices, which made iteration slower and raised the cost of regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Handling Was Less Predictable Than Expected
&lt;/h2&gt;

&lt;p&gt;Not all failures surfaced as explicit errors. In some cases, browser APIs would enter a pending state that neither resolved nor rejected for an extended period of time. From the user’s perspective, the login just appeared stuck.&lt;/p&gt;

&lt;p&gt;We ended up implementing our own timeout mechanism around passkey operations. This was not part of the original plan, but it became necessary to keep the UI responsive and to provide a clear fallback to password-based login when something went wrong.&lt;/p&gt;

&lt;p&gt;Once timeouts were in place, we could fail fast, surface meaningful feedback, and avoid trapping users in a broken flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging in Production Required New Discipline
&lt;/h2&gt;

&lt;p&gt;Supporting passkeys after rollout was harder than shipping them. Much of the execution path lives inside the browser and the operating system, which limits what you can directly observe.&lt;/p&gt;

&lt;p&gt;When users reported issues, they often could not describe what failed beyond saying that the login did not work. Server-side errors alone were not enough to diagnose these cases.&lt;/p&gt;

&lt;p&gt;To compensate, we added logging at every meaningful boundary in the passkey flow. Challenge creation, request dispatch, client response receipt, verification steps, and final resolution. Over time, these logs became essential for identifying platform-specific issues and spotting patterns across devices.&lt;/p&gt;

&lt;p&gt;Looking back, the protocol was not the hard part. Making the system observable and debuggable across platforms was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Passkeys improved our login experience, but they did not simplify authentication as a whole. Passwords are still part of the system, and that is intentional. Treating passkeys as an additive flow rather than a replacement gave us flexibility during rollout and reduced risk when things went wrong.&lt;/p&gt;

&lt;p&gt;After shipping passkeys, we saw roughly a 60 percent improvement in login efficiency. That number is directional and very much dependent on how you measure it, but it aligned with what we saw anecdotally. Fewer retries, faster re-authentication, and less visible frustration during login.&lt;/p&gt;

&lt;p&gt;The main shift for the team was not technical complexity, but operational thinking. More of the flow lives in the browser and the OS, which means less control and fewer guarantees. Logging, timeouts, and conservative assumptions mattered more than protocol correctness.&lt;/p&gt;

&lt;p&gt;Passkeys are worth considering if repeated authentication is a real source of friction in your product. They work best when integrated deliberately, with clear fallbacks and realistic expectations. Like most changes to core infrastructure, the gains come from careful integration, not from the technology alone.&lt;/p&gt;

</description>
      <category>passkeys</category>
      <category>webauthn</category>
      <category>authentication</category>
      <category>security</category>
    </item>
  </channel>
</rss>
