<?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: Shantanu Gonade</title>
    <description>The latest articles on DEV Community by Shantanu Gonade (@shangonade).</description>
    <link>https://dev.to/shangonade</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%2F3984633%2Fd59baecc-fb4c-4ee4-9a2b-4c9c0c694823.png</url>
      <title>DEV Community: Shantanu Gonade</title>
      <link>https://dev.to/shangonade</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shangonade"/>
    <language>en</language>
    <item>
      <title>The EventBridge Myth That Hid a Double-Registration Bug and a Race Condition in My Serverless App</title>
      <dc:creator>Shantanu Gonade</dc:creator>
      <pubDate>Wed, 17 Jun 2026 08:03:46 +0000</pubDate>
      <link>https://dev.to/shangonade/the-eventbridge-myth-that-hid-a-double-registration-bug-and-a-race-condition-in-my-serverless-app-31bi</link>
      <guid>https://dev.to/shangonade/the-eventbridge-myth-that-hid-a-double-registration-bug-and-a-race-condition-in-my-serverless-app-31bi</guid>
      <description>&lt;p&gt;&lt;em&gt;How a routine architecture review turned up a team myth about EventBridge and a double-booking bug hiding in plain sight — and why the two were the same problem wearing different hats.&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%2Fqxwbq1i41994yics1gpb.gif" 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%2Fqxwbq1i41994yics1gpb.gif" alt="I Asked an AI to Audit My “Production-Ready” Serverless App. It Found a Myth and a Race Condition."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three weeks ago, I closed out a 14-week build of &lt;a href="https://github.com/shantanu-gonade/terrapin-events.git" rel="noopener noreferrer"&gt;&lt;strong&gt;TEMS&lt;/strong&gt;&lt;/a&gt; (Terrapin Events Management System) — a serverless event management platform for the University of Maryland. Students browse and register for campus events, organizers manage capacity and waitlists, admins approve events and watch a metrics dashboard. Under the hood: a single-table DynamoDB design, an AppSync GraphQL API, Cognito auth, EventBridge wiring eight backend services together, 54 Lambda functions, the works.&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%2Fx1iw7nby09hl9bdbez75.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%2Fx1iw7nby09hl9bdbez75.png"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;TEMS high-level architecture: AppSync → Lambda → DynamoDB, with EventBridge wiring eight backend services and Cognito handling auth. 54 functions, one custom bus, one table.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The completion report I wrote for myself said what every completion report says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;- 100% of planned features implemented&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- AWS Well-Architected Framework compliance&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- Production-grade security and scalability&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Project Status: COMPLETE AND PRODUCTION-READY&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I believed it. I’d written tests. I’d followed the patterns from the AWS docs I’d read. I’d even left myself meeting notes during the build with little architecture decisions and “why we did it this way” rationales — including one note that EventBridge would “automatically deduplicate events within 24 hours,” so a few rough edges in the registration flow weren’t a big deal.&lt;/p&gt;

&lt;p&gt;Then, mostly out of curiosity, I pointed an AI architecture audit at the codebase and asked it to compare three things against each other: what the docs &lt;em&gt;claimed&lt;/em&gt;, what the code &lt;em&gt;actually did&lt;/em&gt;, and what AWS’s current (2026) guidance recommends for each pattern.&lt;/p&gt;

&lt;p&gt;It came back with a numbered list of findings. Two of them stopped me cold — not because they were exotic, but because they were the kind of thing a senior reviewer catches in five minutes and a solo developer (me, several months deep in feature work) walks past a hundred times without seeing.&lt;/p&gt;

&lt;p&gt;This is the story of those two findings, and why they turned out to be the same bug.&lt;/p&gt;
&lt;h3&gt;
  
  
  The setup: as-described vs. as-built vs. best-practice
&lt;/h3&gt;

&lt;p&gt;Before the findings, a quick word on the audit itself, because the &lt;em&gt;shape&lt;/em&gt; of it is as useful as the content.&lt;/p&gt;

&lt;p&gt;I didn’t ask for a generic “review my AWS architecture” pass — that tends to produce a list of platitudes (&lt;em&gt;“consider adding monitoring,” “review your IAM policies”&lt;/em&gt;) that are true of literally every AWS account on Earth. Instead, I gave it three things to triangulate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What the project documentation claims&lt;/strong&gt;  — architecture docs, meeting notes, the completion report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the code actually does&lt;/strong&gt;  — every serverless.yml, every handler, the GraphQL schema, the DynamoDB table definition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What AWS currently recommends&lt;/strong&gt;  — for each pattern in use (EventBridge, DynamoDB single-table design, idempotency, CQRS-style read models).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then I asked it to flag every place those three things disagreed, with file and line citations for every claim.&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%2F5uiwjjhcazi1malarw5b.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%2F5uiwjjhcazi1malarw5b.png" alt="The audit’s three-way triangulation: what the docs claim, what the code does, what AWS currently recommends. Every finding lives in the gaps."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That last part matters. An AI that says “your event-driven architecture could be more robust” is not useful. An AI that says &lt;em&gt;“&lt;/em&gt;&lt;em&gt;register.ts:161-199 writes two&lt;/em&gt; &lt;em&gt;PutCommands keyed on a freshly generated&lt;/em&gt; &lt;em&gt;registrationId, with no&lt;/em&gt; &lt;em&gt;ConditionExpression enforcing uniqueness on&lt;/em&gt; &lt;em&gt;(eventId, userId) — here's the AWS doc on conditional writes that addresses this"&lt;/em&gt; is useful, because you can go open that file and check it yourself in thirty seconds. Good audits — human or AI — are falsifiable. If you can't verify a finding against the actual repo, throw it out.&lt;/p&gt;

&lt;p&gt;With that framing, here’s what came back.&lt;/p&gt;
&lt;h3&gt;
  
  
  Finding 1: “EventBridge automatically deduplicates events”
&lt;/h3&gt;

&lt;p&gt;Buried in my own meeting notes from the week I wired up the registration flow was this line, paraphrased: &lt;em&gt;EventBridge automatically deduplicates events, so duplicate&lt;/em&gt; &lt;em&gt;RegistrationCreated events within a 24-hour window won't cause problems downstream.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I have no idea anymore where I picked that up. Maybe I was thinking of SQS FIFO queues and their 5-minute deduplication window. Maybe I half-remembered something about EventBridge Pipes. Either way, it had the structure of a fact: specific (24 hours), plausible (AWS services &lt;em&gt;do&lt;/em&gt; dedupe things sometimes), and — crucially — comforting. If it’s true, a bunch of edge cases I hadn’t fully thought through just… go away.&lt;/p&gt;

&lt;p&gt;The audit’s response was blunt: &lt;strong&gt;Amazon EventBridge provides at-least-once delivery and has no native event-level deduplication.&lt;/strong&gt; None. Not in 24 hours, not ever. If a producer calls PutEvents twice with logically identical payloads, EventBridge will happily deliver both, to every matching rule, as two separate events. (There's PutEvents request-level retry behavior if &lt;em&gt;the API call itself&lt;/em&gt; fails and you retry it, but that's a different thing from "the same business event was generated twice by my own application code," which is the case that actually matters here.)&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%2Fq9frojoxysqx0r4p47r1.gif" 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%2Fq9frojoxysqx0r4p47r1.gif" alt="EventBridge at-least-once delivery: two identical PutEvents calls produce two deliveries to every matching rule. There is no 24-hour deduplication window — that's SQS FIFO, and even that's 5 minutes."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For TEMS specifically, that means every consumer of RegistrationCreated, WaitlistAdded, WaitlistPromoted, and friends — published from register.ts:224-242 and waitlist-manager.ts onto our custom bus (TemsEventBus, with a 30-day replay archive and a DLQ behind it) — has to assume it might receive the &lt;em&gt;same logical event twice&lt;/em&gt; and either be naturally idempotent (a second "send confirmation email" is harmless-ish, if annoying) or actively de-duplicate (a second "decrement available capacity" is not harmless at all).&lt;/p&gt;

&lt;p&gt;On its own, this is a “fix the docs, add a code comment” finding. Mildly embarrassing, low stakes. I’d have fixed the meeting note, added a // NOTE: EventBridge does NOT dedupe — see  comment near the PutEventsCommand calls, and moved on.&lt;/p&gt;

&lt;p&gt;Except the audit’s very next finding was about the registration handler. And once I read it, the meeting-note myth stopped being mildly embarrassing and started looking like the &lt;em&gt;reason&lt;/em&gt; the second bug had survived this long.&lt;/p&gt;
&lt;h3&gt;
  
  
  Finding 2: the double-registration bug
&lt;/h3&gt;

&lt;p&gt;Here’s a simplified version of what register.ts does when a student registers for an event (real file: &lt;a href="https://github.com/shantanu-gonade/terrapin-events/blob/68604c25503e6397173aa7c923ece11aeb4f459e/backend/services/registrations/handlers/register.ts" rel="noopener noreferrer"&gt;backend/services/registrations/handlers/register.ts&lt;/a&gt;):&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&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%2Fsnpi8t08vk5wbbfl4m3g.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%2Fsnpi8t08vk5wbbfl4m3g.png" alt="The race window: Requests A and B both pass the isUserRegistered read-check before either write lands. Both generate unique registrationIds. DynamoDB accepts both unconditionally. One student, two records, two QR codes."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read in isolation, every individual piece looks &lt;em&gt;reasonable&lt;/em&gt;. There’s an idempotency check! There’s a “already registered” check! There’s an atomic counter increment using DynamoDB’s ADD update expression, which really is atomic! It reads like code written by someone who knew about these problems and was actively guarding against them. (It was. That someone was me, four months ago, clearly aware idempotency was a thing — just not finishing the thought.)&lt;/p&gt;

&lt;p&gt;But walk through what actually happens when a student double-clicks “Register” — or, more realistically, taps it once on flaky dorm wifi, the request hangs, they tap again:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Request A&lt;/strong&gt; arrives. No idempotencyKey (the frontend doesn't currently send one — it's optional, remember). isUserRegistered runs its Query, finds nothing, returns false. Capacity check passes. A is now mid-flight, about to write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request B&lt;/strong&gt; arrives a few hundred milliseconds later, before A’s PutCommands have landed. isUserRegistered runs &lt;em&gt;again&lt;/em&gt; — and because A hasn't written yet, B &lt;em&gt;also&lt;/em&gt; gets false. Capacity check passes again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both requests&lt;/strong&gt; generate their own fresh registrationId via nanoid(16). Both write two PutCommands each — USER#{userId}/REGISTRATION#{id} and EVENT#{eventId}/REGISTRATION#{id} — with completely different sort keys, because the IDs are different random strings. &lt;strong&gt;Nothing in DynamoDB rejects this.&lt;/strong&gt; There's no ConditionExpression. There's no uniqueness constraint anywhere that says "a given (eventId, userId) pair may only have one active registration row."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both requests&lt;/strong&gt; call atomicIncrementRegistered(eventId, 1). This part &lt;em&gt;is&lt;/em&gt; atomic — it's a DynamoDB UpdateItem with ADD registeredCount :inc — so the counter correctly goes up by 2. Correctly, for the wrong reason: it's accurately counting two registrations that should never have both existed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both requests&lt;/strong&gt; publish a RegistrationCreated event to EventBridge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;End state: one team, two registration records, two QR codes, two confirmation emails, and an event capacity counter that’s now off by one in the direction of “oversold.” Do this across a popular event during a registration rush — say, the first ten minutes after a big lecture’s extra-credit event opens up — and you get a slow leak of phantom registrations that nobody notices until check-in day, when the room is fuller than the headcount predicted and a few students’ QR codes scan into a now-”full” event that capacity-checks are blocking new registrants from.&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%2Fww1uozdos94oao57t97a.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%2Fww1uozdos94oao57t97a.png" alt="End state after a successful double-registration: two rows in DynamoDB, a capacity counter incremented by 2, two RegistrationCreated events on the bus, and two confirmation emails in the student's inbox."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The race window is small — milliseconds — but it doesn’t need to be wide. It needs to be &lt;em&gt;non-zero&lt;/em&gt;, and it needs traffic. A campus event platform at registration-rush time has both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the two findings meet
&lt;/h3&gt;

&lt;p&gt;Here’s the part that actually changed how I think about this codebase.&lt;/p&gt;

&lt;p&gt;The “fix” for the double-registration bug is, fundamentally, &lt;em&gt;idempotency at the write layer&lt;/em&gt;: make it so that no matter how many times “register user X for event Y” is requested, at most one registration record can ever exist for that pair. That’s a DynamoDB-side guarantee — a conditional write or a transaction — and it’s completely independent of anything downstream.&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%2Fysf8ptks3e9zgyi40b8g.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%2Fysf8ptks3e9zgyi40b8g.png" alt="The EventBridge Myth: Think AWS handles your event deduplication automatically? Think again. Why "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The myth said: &lt;em&gt;“don’t worry about duplicate events, EventBridge will collapse them within 24 hours.”&lt;/em&gt; If I’d genuinely believed that and stopped there, I’d have been solving the wrong layer entirely — even in a world where EventBridge &lt;em&gt;did&lt;/em&gt; dedupe events, that would do nothing to stop the DynamoDB writes in steps 3 and 4 above from happening twice. The duplication isn’t an EventBridge problem. It’s not even really a “two Lambda invocations” problem. It’s that &lt;strong&gt;the database has no opinion about whether a given&lt;/strong&gt; &lt;strong&gt;(eventId, userId) pair is allowed to appear more than once&lt;/strong&gt;, and nothing upstream of the database closes that gap.&lt;/p&gt;

&lt;p&gt;The myth didn’t &lt;em&gt;cause&lt;/em&gt; the bug. The bug would exist with or without it. But I think the myth is a big part of why the bug survived a full build cycle, a test suite with &amp;gt;80% coverage, and a “production-ready” sign-off — because every time some part of my brain noticed “hm, this registration flow doesn’t have a hard uniqueness guarantee,” another part of my brain had a half-remembered, comforting, &lt;em&gt;wrong&lt;/em&gt; answer ready: &lt;em&gt;that’s fine, it gets cleaned up downstream.&lt;/em&gt; Nothing gets cleaned up downstream. There is no downstream cleanup. There never was.&lt;/p&gt;

&lt;p&gt;That’s the actual lesson, more than either finding individually: &lt;strong&gt;a wrong belief about how your infrastructure behaves doesn’t just cost you when you act on it directly — it costs you every time it talks you out of fixing something else.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix (briefly — full tutorial is next)
&lt;/h3&gt;

&lt;p&gt;The shape of the fix is: give every (eventId, userId) pair a single, deterministic "claim" item —&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`EVENT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`REGCLAIM#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— and write it with ConditionExpression: 'attribute_not_exists(PK)', ideally inside the same TransactWriteItems call that creates the two registration records and the idempotency record. The second concurrent request's conditional write fails with a ConditionalCheckFailedException, the handler catches that and returns the &lt;em&gt;existing&lt;/em&gt; registration instead of creating a new one, and the race window closes — not because it got narrower, but because DynamoDB itself now rejects the collision regardless of timing.&lt;/p&gt;

&lt;p&gt;There’s also a more turnkey option: AWS Lambda Powertools’ &lt;a href="https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/idempotency/" rel="noopener noreferrer"&gt;Idempotency utility&lt;/a&gt;, which wraps a handler with makeIdempotent() and a DynamoDB-backed persistence layer, handling the claim-record lifecycle, TTLs, and in-flight request coalescing for you. Making idempotencyKey a &lt;em&gt;required&lt;/em&gt; argument (instead of optional, as it is today at register.ts:31 and in the GraphQL schema's registerForEvent(eventId: ID!, idempotencyKey: String)) is part of this too — an idempotency mechanism that callers can opt out of by simply not passing a key isn't really a guarantee, it's a suggestion.&lt;/p&gt;

&lt;p&gt;I’m walking through both approaches — deterministic conditional writes &lt;em&gt;and&lt;/em&gt; the Powertools utility, with before/after code and a way to actually test for the race condition — in the companion piece: &lt;strong&gt;Stop Relying on EventBridge to Dedupe Your Events: A Practical Guide to Idempotent DynamoDB Writes ** soon **.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What I’d tell you if you’re about to run this audit on your own project
&lt;/h3&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%2F91wizdlaxql2yarweasf.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%2F91wizdlaxql2yarweasf.png" alt="The EventBridge Myth: Think AWS handles your event deduplication automatically? Think again. Image 08-square.png breaks down why "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few things I’d do differently next time, and would suggest if you try this on your own codebase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ask for citations, not opinions.&lt;/strong&gt; “Is this secure?” gets you a essay. “Show me every IAM policy with a Resource: '*' and the line number" gets you a punch list. The second one is useful at 11pm before a launch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate “as described” from “as built” from “current best practice,” explicitly.&lt;/strong&gt; A surprising amount of value in this audit came from the gap &lt;em&gt;between&lt;/em&gt; my docs and my code — not from either one alone. My own completion report claimed 5 backend services; the code has 8. That’s not a security issue, but it’s a signal that the docs were written from memory, not from the repo — which made me trust the &lt;em&gt;other&lt;/em&gt; claims in those docs less, including the EventBridge note.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify anything time-sensitive against current docs, especially for fast-moving services.&lt;/strong&gt; AWS behavior and tooling changes. The Lambda Powertools Idempotency utility I’m using in the next article didn’t always look like it does now. An audit’s value decays; treat findings as a snapshot, not gospel, and re-check the load-bearing ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The boring findings are often connected to the interesting ones.&lt;/strong&gt; I almost filed the EventBridge note under “fix the comment, move on.” It turned out to be load-bearing for a much bigger miss. If an audit turns up something that smells like “minor docs inaccuracy,” ask what &lt;em&gt;decisions&lt;/em&gt; were made on the assumption that the inaccurate thing was true.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TEMS is still “production-ready” in the sense that matters most: it’s a real system, serving a real use case, and I know exactly what’s wrong with it and how to fix it — which is a state most production systems never quite reach. The completion report needed an asterisk, not a rewrite. But I’m glad I asked the question before someone’s registration silently doubled during finals week.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;TEMS is a serverless event management platform built on AWS Lambda, AppSync, DynamoDB, EventBridge, and Cognito. This is the first in a short series on what a structured AI architecture audit found in a “finished” project — next up, the idempotency fix in detail, and a look at how the same FIFO waitlist that handles oversold events is built entirely on DynamoDB.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A Note of Appreciation
&lt;/h3&gt;

&lt;p&gt;This project, and this write-up, wouldn’t exist without the people who supported it along the way.&lt;/p&gt;

&lt;p&gt;To &lt;a href="https://www.linkedin.com/in/drtonydbarber/" rel="noopener noreferrer"&gt;&lt;strong&gt;Dr. Tony D. B&lt;/strong&gt;&lt;/a&gt;. — thank you for your guidance, your patience, and for holding the work to a standard that made it worth doing properly. Your feedback shaped not just the system, but how I think about building one.&lt;/p&gt;

&lt;p&gt;To &lt;a href="https://www.linkedin.com/in/rohin-vaidya/" rel="noopener noreferrer"&gt;&lt;strong&gt;Rohin Vaidya&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/sanskar-vidyarthi/" rel="noopener noreferrer"&gt;&lt;strong&gt;Sanskar Vidyarthi&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/affansyedmuhammad/" rel="noopener noreferrer"&gt;&lt;strong&gt;Syed Muhammad Affan&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/vishalrameshpatil/" rel="noopener noreferrer"&gt;&lt;strong&gt;Vishal Patil&lt;/strong&gt;&lt;/a&gt; — thank you for your contributions, your collaboration, and for showing up consistently through every phase of this build. The best parts of TEMS reflect what we figured out together.&lt;/p&gt;

&lt;p&gt;I’m grateful to all of you for the time, thoughtfulness, and care you brought to this. It meant more than I can neatly put into a completion report.&lt;/p&gt;

&lt;p&gt;Git Repo: &lt;a href="https://github.com/shantanu-gonade/terrapin-events" rel="noopener noreferrer"&gt;https://github.com/shantanu-gonade/terrapin-events&lt;/a&gt;&lt;/p&gt;




</description>
      <category>architecture</category>
      <category>aws</category>
      <category>cloudcomputing</category>
      <category>ai</category>
    </item>
    <item>
      <title>I’m an Android Engineer. I got an AWS Solutions Architect Cert. Here’s why?</title>
      <dc:creator>Shantanu Gonade</dc:creator>
      <pubDate>Tue, 16 Jun 2026 17:08:23 +0000</pubDate>
      <link>https://dev.to/shangonade/im-an-android-engineer-i-got-an-aws-solutions-architect-cert-heres-why-1p23</link>
      <guid>https://dev.to/shangonade/im-an-android-engineer-i-got-an-aws-solutions-architect-cert-heres-why-1p23</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%2F924%2F0%2A1T6bLzjNMwxwh0MU" 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%2F924%2F0%2A1T6bLzjNMwxwh0MU" width="924" height="519"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Bridge: mapping Android architecture to AWS cloud architecture, side by side&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For about a year, the same question kept coming back, usually with a slightly puzzled tilt to it: &lt;em&gt;why is an Android engineer studying AWS?&lt;/em&gt; I was 3.7 years into writing Kotlin at Samsung — OEM-level Android, Compose, MVVM, coroutines, the works — and here I was, drawing VPC diagrams and memorizing DynamoDB capacity modes for the Solutions Architect — Associate exam. From the outside it looked like a detour. “Go deeper on Android,” people said. “That’s your lane.”&lt;/p&gt;

&lt;p&gt;I want to tell you about the realization that made me ignore that advice, because it turned out to be the opposite of a detour. Studying cloud architecture made me a measurably better Android engineer — and the reason is something I now believe most people on both sides of the mobile/backend line miss.&lt;/p&gt;
&lt;h3&gt;
  
  
  The moment it clicked
&lt;/h3&gt;

&lt;p&gt;The honest origin story isn’t glamorous. It started with a retry.&lt;/p&gt;

&lt;p&gt;I was debugging a sync bug in a feature that talked to a backend I hadn’t built. The client would fire a write, the write would occasionally fail, and my retry logic would dutifully fire it again — and sometimes the user would end up with the operation applied &lt;em&gt;twice&lt;/em&gt;. I’d written the client the way a mobile engineer writes a client: assume the request either succeeds or fails, and if it fails, try again. Clean. Reasonable. Wrong.&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%2F924%2F0%2A1qGpRvpAfhp1TRcf" 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%2F924%2F0%2A1qGpRvpAfhp1TRcf" width="924" height="520"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A mobile retry bug caused by missing idempotency — why a successful server write got duplicated by a client retry.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because the thing I hadn’t internalized was what happened on the &lt;em&gt;other side of the wire&lt;/em&gt;. The request had succeeded — the server had committed it — and only the response had been lost on a flaky connection. My retry wasn’t recovering from a failure; it was duplicating a success. The fix wasn’t in my Kotlin. It was in understanding that the backend needed to be idempotent, and that my client needed to send a key the server could use to recognize “I’ve already seen this one.” That’s a distributed-systems concept, and I was treating it as a networking annoyance.&lt;/p&gt;

&lt;p&gt;That bug embarrassed me a little, and embarrassment is a good teacher. I realized I’d been doing something subtly limiting for years: I treated the backend as a vending machine. I put a request in, I got a response out, and the inside of the machine was somebody else’s problem. But my app’s behavior — how it retried, how it paginated, how it cached, how it degraded offline — was entirely shaped by a system I refused to reason about. Mobile is increasingly full-stack. Mobile DevOps — the CI/CD, the release infrastructure, the build pipelines I work in every day — &lt;em&gt;lives in the cloud&lt;/em&gt;. I kept bumping into the backend and choosing to look away. The cert was me deciding to look.&lt;/p&gt;
&lt;h3&gt;
  
  
  The realization underneath the realization
&lt;/h3&gt;

&lt;p&gt;Here’s the part I didn’t see coming. I expected studying AWS to teach me about the backend. It did. What I didn’t expect was that it would re-explain &lt;em&gt;my own job to me&lt;/em&gt; — because senior Android architecture and cloud architecture are, structurally, the same mental model wearing different clothes.&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%2Fm9wyqlbjkdmfcyhkacaa.gif" 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%2Fm9wyqlbjkdmfcyhkacaa.gif" width="480" height="270"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Animated map of all 7 Android-to-AWS architecture parallels, from ViewModel/Lambda to Hilt/Infrastructure-as-Code&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The skill underneath both isn’t “Android” and it isn’t “AWS.” It’s drawing boundaries: between state and effects, between producers and consumers, between the read path and the write path, between what you declare and what the framework assembles. Once I saw that, the AWS material stopped feeling foreign and started feeling like my own architecture diagrams with the labels swapped. Let me walk the parallels, because this is the actual payload of the post.&lt;/p&gt;
&lt;h3&gt;
  
  
  Parallel 1 — the stateless state holder
&lt;/h3&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%2F924%2F0%2A5zDn_7ClggPOJ-UM" 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%2F924%2F0%2A5zDn_7ClggPOJ-UM" width="924" height="924"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Parallel 1: Android ViewModel vs. AWS Lambda — both stateless handlers that turn input into output&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In modern Android, a ViewModel exposes an immutable UiState and holds no long-lived mutable state of its own. The UI sends events up; the ViewModel reduces them into a new state and emits it down. It’s a function from input to output with a thin lifecycle wrapper. That’s &lt;em&gt;why&lt;/em&gt; it’s the easy part of the app to unit-test — you hand it an input, you assert on the output, no environment to stand up.&lt;/p&gt;

&lt;p&gt;A Lambda handler is the same shape. It holds no durable state between invocations; it takes an event in, derives a response, and returns. Anything it needs to persist goes &lt;em&gt;out&lt;/em&gt; to a store. It’s testable for exactly the same reason a ViewModel is: input in, output out.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Android — ViewModel as a stateless-ish state holder&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CartViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CartRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;_uiState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CartUiState&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;uiState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CartUiState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_uiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asStateFlow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CartAction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;CartAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AddItem&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_uiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="c1"&gt;// events in → new immutable state out. No hidden mutable state.&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;tyt&lt;/span&gt;

&lt;span class="c1"&gt;// AWS - Lambda handler: same shape, different runtime&lt;/span&gt;
&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AddItemEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CartResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// event in → response out. No durable state held between invocations.&lt;/span&gt;
  &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyAddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nc"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// persistence goes OUT&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;updated&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;Look at the two and the resemblance is almost rude. Both refuse to hold long-lived mutable state. Both push persistence to the edge. Both are pure-ish cores wrapped in a platform-managed lifecycle. The day I saw the Lambda and the ViewModel as the same object, AWS stopped being a new subject.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel 2 — model data for the queries you actually make
&lt;/h3&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%2F924%2F0%2AN9zqNba56kKDfQqx" 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%2F924%2F0%2AN9zqNba56kKDfQqx" width="924" height="924"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Parallel 2: Room offline-first database vs. DynamoDB single-table design — model your schema for the queries you run&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Offline-first Android puts a local Room database as the source of truth: the UI reads from Room, a sync layer reconciles Room with the remote, and the app stays usable on a subway with no signal. The non-negotiable rule of doing this well is that you model your tables for the queries the screens actually run — not for some clean normalized picture of the domain. If a screen needs “the last message per conversation, sorted by time,” you shape the data so that’s one cheap query, not a join you reassemble in Kotlin.&lt;/p&gt;

&lt;p&gt;DynamoDB single-table design is that rule with the volume turned up. There are no joins; you list every access pattern up front and design partition/sort keys so each one is a single, cheap query, then add a read-model cache for the hottest paths. Same discipline, same trap: people who model DynamoDB like a relational schema hate it, exactly like people who model Room around entities instead of screens end up with sluggish lists and N+1 reads.&lt;/p&gt;

&lt;p&gt;Both are the same instruction: the questions you’ll ask of the data are the design input. The schema falls out of the questions, not the other way around. I’d been doing this on the client for years without naming it. AWS gave me the name and showed me it scales all the way up.&lt;/p&gt;
&lt;h3&gt;
  
  
  Parallel 3 — decouple the producer from the consumer
&lt;/h3&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%2F924%2F0%2Aq09qJKtPVNnhu-xP" 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%2F924%2F0%2Aq09qJKtPVNnhu-xP" width="924" height="924"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Parallel 3: Kotlin Flow vs. AWS EventBridge/SQS — decoupling producers from consumers with backpressure and queue-based load leveling&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Compose architecture runs on unidirectional data flow: events down, state up, with Kotlin Flow as the reactive spine. A producer emits; collectors react; and crucially, Flow gives you backpressure — a slow collector doesn’t force the producer to block or drop on the floor, it buffers or conflates on defined terms.&lt;/p&gt;

&lt;p&gt;The backend twin is event-driven architecture. A service does its work and emits a domain event onto EventBridge (or drops a message on SQS); other services subscribe and react on their own time. The producer doesn’t call the consumer and doesn’t wait on it. And the queue does the same job Flow’s backpressure does on the client: SQS queue-based load leveling absorbs a burst so a spike in producers doesn’t knock over the consumers — you bound concurrency instead of overwhelming the downstream.&lt;/p&gt;

&lt;p&gt;There’s a deeper structural-concurrency rhyme here too. Coroutines’ structured concurrency bounds and scopes the work a screen is allowed to spawn — when the scope dies, the children die, no leaks. Queue-based leveling bounds and scopes the work the backend is allowed to run at once. Both are answers to the same question: &lt;em&gt;how do I absorb bursts without unbounded concurrency taking the system down?&lt;/em&gt; I started writing better Flow code — conflating where I should, choosing the right buffer strategy — because SQS made me think explicitly about what a consumer does when the producer outruns it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Parallel 4 — declare your dependencies, let the framework assemble them
&lt;/h3&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%2F924%2F0%2A6m6b9kd2RRGqP4bM" 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%2F924%2F0%2A6m6b9kd2RRGqP4bM" width="924" height="924"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Parallel 4: Hilt dependency injection vs. AWS Infrastructure-as-Code — declare what you need, let the framework wire it&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the parallel that surprised me most, because the two technologies look nothing alike on the surface.&lt;/p&gt;

&lt;p&gt;Hilt (Dagger) is a dependency-injection graph. You don’t construct your Repository and hand-thread its ApiService and Database through five constructors; you declare how each thing is provided, and the framework assembles the graph and injects what each consumer asked for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Android — Hilt: declare the dependency, the framework wires the graph&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;InstallIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SingletonComponent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;object&lt;/span&gt; &lt;span class="nx"&gt;DataModule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Provides&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Singleton&lt;/span&gt;
    &lt;span class="nx"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;provideCartRepo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CartApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppDatabase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CartRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="nc"&gt;CartRepositoryImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// you declare; Hilt assembles &amp;amp; injects&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Infrastructure-as-Code is the same idea pointed at cloud resources. You don’t click through a console wiring a Lambda to a DynamoDB table to an IAM role by hand; you declare the resources and their relationships, and the framework (CloudFormation / the Serverless Framework / Terraform) assembles the graph and provisions what each piece needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nc"&gt;AWS&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nc"&gt;IaC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;declare&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;framework&lt;/span&gt; &lt;span class="n"&gt;provisions&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;wires&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;
&lt;span class="n"&gt;functions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;addItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;declare&lt;/span&gt; &lt;span class="n"&gt;what&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;framework&lt;/span&gt; &lt;span class="n"&gt;grants&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="n"&gt;dependency&lt;/span&gt; &lt;span class="n"&gt;below&lt;/span&gt;
    &lt;span class="n"&gt;iamRoleStatements&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="nc"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Allow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;PutItem&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nc"&gt;GetAtt&lt;/span&gt; &lt;span class="nc"&gt;CartTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Arn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are the same move: stop assembling dependencies by hand; declare them and let a framework build the graph. Hilt’s @Provides and an IaC resource block are the same sentence in two languages. After IaC, I stopped resenting Hilt’s boilerplate and started seeing it as the client-side instance of a pattern I now trusted at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fifth one — boundaries that scale a team
&lt;/h3&gt;

&lt;p&gt;I’ll name a fifth quickly because it’s the one senior engineers feel most. Gradle multi-module boundaries — :feature:cart, :core:network, an api module a :feature depends on versus the impl it doesn’t see — exist to enforce boundaries so a codebase and a team can scale without everything depending on everything. Microservice and service boundaries on the backend are the identical concern: you split so teams can ship independently and a change’s blast radius is bounded. The repository pattern abstracting your data sources is, on the backend, an API gateway or service layer abstracting the systems behind it. Same instinct, same payoff, same failure mode when you draw the lines wrong.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where the analogy breaks — and why that’s the most valuable part
&lt;/h4&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%2F800%2F0%2A9tf2omLUZkt75USy" 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%2F800%2F0%2A9tf2omLUZkt75USy" width="800" height="1000"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Where the Android-AWS analogy breaks: cold starts, distributed failure, and eventual consistency&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If I stopped here, this would be a tidy little “everything is the same” essay, and that would be dishonest. The analogy is powerful precisely up to the point where it shatters, and the shattering is where the cert earned its keep.&lt;/p&gt;

&lt;p&gt;Cold starts don’t exist on a device. Your ViewModel is already in memory; your code is warm. A Lambda can be cold — a fresh container, an init penalty, a p99 that’s nothing like your p50. On a device you optimize for jank and battery; in the cloud you optimize for an entirely different latency distribution, one with a long tail your client will &lt;em&gt;feel&lt;/em&gt;. Understanding that tail changed how I design clients: I now expect the occasional slow first response and build the UI to stay responsive through it, instead of assuming the backend is as warm as my ViewModel.&lt;/p&gt;

&lt;p&gt;Partial and distributed failure barely exist on a device. On the client, an operation mostly succeeds or fails as a unit, locally, observably. In a distributed system, a request can succeed on the server and fail to reach the client (my original retry bug), or half a fan-out can complete while the other half is still in flight. There’s no single “did it work?” you can read. This is why idempotency and explicit retry semantics aren’t backend trivia — they’re contracts my mobile client has to participate in. I write retries differently now: with idempotency keys, with the assumption that “no response” does not mean “no effect.”&lt;/p&gt;

&lt;p&gt;Eventual consistency isn’t a thing your screen normally has to model. A device reads its own writes from local storage instantly. A distributed read replica might not have caught up yet. Designing for that — knowing that “I just wrote it but the read came back stale” is &lt;em&gt;correct behavior&lt;/em&gt;, not a bug — has no real equivalent in single-device Android. But the moment your app reads from a system that’s eventually consistent, your UI has to account for it: optimistic updates, reconciliation, “pending” states. The cert is what let me see that coming instead of filing a phantom bug.&lt;/p&gt;

&lt;p&gt;These three — cold starts, distributed failure, eventual consistency — are the load-bearing differences. And here’s the thing: learning where the parallel ends is exactly what made me a better mobile engineer. Not the similarities — the similarities just made the material learnable fast. The &lt;em&gt;differences&lt;/em&gt; are what let me design clients that cooperate with the system they live inside: offline-first sync that tolerates eventual consistency, retries that assume idempotency, pagination and caching tuned to a real latency tail rather than an imagined instant backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this makes me stronger on both sides
&lt;/h3&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%2F924%2F0%2A9rck62ssVhqNBp00" 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%2F924%2F0%2A9rck62ssVhqNBp00" width="924" height="520"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Full-stack architecture diagram: Android client (Kotlin, Compose, Hilt) connected to a serverless AWS backend (Lambda, DynamoDB, EventBridge)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I came out of the Solutions Architect cert a better Android engineer, and it’s not the line on the résumé — it’s the second pair of eyes. When I design a screen now, I’m also thinking about the shape of the system feeding it: what its consistency guarantees are, where it can be slow, how it fails, what it costs to scale. When I reason about a backend, I bring the client’s reality — that latency and failure aren’t abstractions, they’re something a human is staring at on a phone, waiting.&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%2Fjc3t24stufs1xy9nndi2.gif" 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%2Fjc3t24stufs1xy9nndi2.gif" width="432" height="540"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Realization: It’s the same mental map&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I’ve now built both directions for real — a fully serverless app on AWS (single-table DynamoDB, 54 Lambdas, EventBridge) and OEM-level Android at Samsung — and the throughline is that the architecture instincts transfer almost completely, &lt;em&gt;except&lt;/em&gt; at the three seams above, which are precisely the seams you have to get right. Mobile is going full-stack whether mobile engineers come along or not. The ones who reason about the whole system, instead of treating the backend as a vending machine, are going to build clients that feel inexplicably more solid than everyone else’s. That’s the bet I made. So far it’s paying off in cleaner retries, smarter offline behavior, and a much shorter conversation with backend engineers when something’s on fire.&lt;/p&gt;

&lt;p&gt;If you write mobile and the backend still feels like a black box: pick the one cert or the one project that forces you to open it. You won’t come back a backend engineer. You’ll come back a better mobile one.&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%2F924%2F0%2ApG846fhZnphCbizp" 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%2F924%2F0%2ApG846fhZnphCbizp" width="924" height="520"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Recap diagram: all 7 Android-AWS architecture parallels and the 3 places the analogy breaks, in one map&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Parallels mapped: ViewModel↔Lambda · Room offline-first↔DynamoDB single-table+cache · UDF/Kotlin Flow↔EventBridge/reactive streams · structured concurrency↔SQS load leveling · Hilt DI↔Infrastructure-as-Code · Gradle module boundaries↔service boundaries · Repository pattern↔API/service layer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Where it breaks: cold starts · distributed/partial failure · eventual consistency.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I’m Shantanu — Android engineer and AWS Certified Solutions Architect — Associate. I build full-stack — Android clients and the serverless backends behind them. I write about mobile and cloud at&lt;/em&gt; &lt;a href="http://shantanu-gonade.com/" rel="noopener noreferrer"&gt;&lt;em&gt;shantanu-gonade.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;. Open to senior Android and mobile-platform roles — let’s connect.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>mobile</category>
      <category>serverless</category>
      <category>aws</category>
      <category>android</category>
    </item>
    <item>
      <title>Android After Google I/O 2026</title>
      <dc:creator>Shantanu Gonade</dc:creator>
      <pubDate>Tue, 09 Jun 2026 02:13:49 +0000</pubDate>
      <link>https://dev.to/shangonade/android-after-google-io-2026-jhb</link>
      <guid>https://dev.to/shangonade/android-after-google-io-2026-jhb</guid>
      <description>&lt;p&gt;Edge-to-edge is mandatory. Predictive back now owns your stack. Compose 1.11 ships Grid and Styles. Navigation 3 is stable. Kotlin 2.4 context parameters are production-ready.&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%2Fmqzju6r5ky1mq7ff7pkw.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%2Fmqzju6r5ky1mq7ff7pkw.png" alt="Android After Google I/O 2026" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your app targets Android 16 (API 36), two things will break the moment a user upgrades: your status bar layout and your back button handler. Not "might break." Will break.&lt;/p&gt;

&lt;p&gt;Google I/O 2026 has a single through-line: Android is now Compose-first. That phrase showed up in every keynote. It isn't marketing. It means Google is putting all future guidance, new APIs, and tooling investment behind Jetpack Compose. Five years after its launch, Compose has graduated from "recommended" to "the only supported path forward." Every change in this article connects back to that shift.&lt;/p&gt;

&lt;p&gt;Let's start with what's on fire.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two Things That Will Break Your App Right Now
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Edge-to-Edge Is No Longer Optional
&lt;/h4&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%2Fill9swbw0if7p7wqps4l.gif" 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%2Fill9swbw0if7p7wqps4l.gif" alt="Left: not edge-to-edge. Right: edge-to-edge — the only mode Android 16 supports." width="760" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;&lt;em&gt;Left: not edge-to-edge. Right: edge-to-edge — the only mode Android 16 supports.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;Starting with Android 16 (API level 36), the system ignores &lt;code&gt;WindowCompat.setDecorFitsSystemWindows(window, false)&lt;/code&gt;. Apps that used &lt;code&gt;true&lt;/code&gt; to opt out of edge-to-edge will now render incorrectly — content bleeds behind the navigation bar and status bar without insets applied.&lt;/p&gt;

&lt;p&gt;Edge-to-edge is enforced. There is no opt-out flag, no manifest attribute to escape it. You must handle window insets explicitly.&lt;/p&gt;

&lt;p&gt;Here is the correct pattern for a full-screen composable in 2026:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Mandatory Edge-to-Edge Activity initialization for apps targeting Android 16 (API 36) using Jetpack Compose.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;The &lt;code&gt;enableEdgeToEdge()&lt;/code&gt; call replaces the old &lt;code&gt;WindowCompat&lt;/code&gt; dance. It sets the correct window appearance flags, handles light/dark status bar icons, and prepares the window for inset consumption.&lt;/p&gt;

&lt;p&gt;Inside your screens, use &lt;code&gt;Modifier.consumeWindowInsets&lt;/code&gt; and the &lt;code&gt;WindowInsets&lt;/code&gt; APIs directly rather than relying on Scaffold to absorb everything silently:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Proper window insets consumption using Scaffold and WindowInsets.safeContent in Jetpack Compose.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;The key mistake developers make is passing &lt;code&gt;WindowInsets.Zero&lt;/code&gt; to &lt;code&gt;contentWindowInsets&lt;/code&gt; to "fix" visual issues, which defeats the purpose of edge-to-edge and creates problems on devices with gesture navigation. Use &lt;code&gt;WindowInsets.safeContent&lt;/code&gt; and let the system compose correctly.&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%2F6wit89rw8awdbhepkbj3.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%2F6wit89rw8awdbhepkbj3.png" alt="Gesture insets: the green zones are controlled by the OS. Never place tappable targets under them." width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;&lt;em&gt;Gesture insets: the green zones are controlled by the OS. Never place tappable targets under them.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;If you have custom views inside a &lt;code&gt;AndroidView&lt;/code&gt; composable, apply insets explicitly:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Applying system bar window insets manually to a legacy Android View inside an AndroidView composable wrapper.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;&lt;strong&gt;Migration checklist:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace all &lt;code&gt;WindowCompat.setDecorFitsSystemWindows(window, true)&lt;/code&gt; calls with &lt;code&gt;enableEdgeToEdge()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Replace &lt;code&gt;WindowCompat.setDecorFitsSystemWindows(window, false)&lt;/code&gt; calls — they are now no-ops&lt;/li&gt;
&lt;li&gt;Audit every &lt;code&gt;Scaffold&lt;/code&gt; for missing &lt;code&gt;contentWindowInsets&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test on a device running Android 16 with gesture navigation enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Predictive Back: &lt;code&gt;onBackPressed&lt;/code&gt; Is Dead
&lt;/h3&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%2F1hviqn6ef516rgczux51.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%2F1hviqn6ef516rgczux51.png" alt="Predictive back previews the destination before the gesture commits. On Android 16, your app must support this natively." width="800" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;&lt;em&gt;Predictive back previews the destination before the gesture commits. On Android 16, your app must support this natively.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;On Android 16 (API 36), &lt;code&gt;onBackPressed()&lt;/code&gt; is no longer called. &lt;code&gt;KeyEvent.KEYCODE_BACK&lt;/code&gt; is not dispatched. If you override &lt;code&gt;onBackPressed&lt;/code&gt; in an Activity or Fragment and you have not migrated to the predictive back APIs, your back handling silently does nothing.&lt;/p&gt;

&lt;p&gt;This is a hard breaking change, not a deprecation warning.&lt;/p&gt;

&lt;p&gt;The correct implementation uses &lt;code&gt;BackHandler&lt;/code&gt; in Compose, which wires into the &lt;code&gt;OnBackPressedDispatcher&lt;/code&gt; — the only supported path:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Intercepting system back gestures with BackHandler and AnimatedContent in a multi-step Jetpack Compose form wizard.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;For the system's predictive back preview animation — the one that shows a shrinking card as you swipe — you get it for free when you use &lt;code&gt;BackHandler&lt;/code&gt;. The system intercepts the gesture and shows the preview before your handler fires. You do not write animation code for the default behavior.&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%2Fmcktebzuowq4glu5aj5i.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%2Fmcktebzuowq4glu5aj5i.png" alt="Motion spec during a left-edge swipe: the surface scales to 90%, shifts right with an 8dp margin. PredictiveBackHandler gives you the progress value to drive this." width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;&lt;em&gt;Motion spec during a left-edge swipe: the surface scales to 90%, shifts right with an 8dp margin. `PredictiveBackHandler` gives you the progress value to drive this.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;For custom back animations (your own content transitioning while the user is mid-swipe), use &lt;code&gt;PredictiveBackHandler&lt;/code&gt;:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Driving bespoke screen scale and translation animations using PredictiveBackHandler progress events in Jetpack Compose.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;If you still have legacy code that uses &lt;code&gt;onBackPressed&lt;/code&gt;, set &lt;code&gt;android:enableOnBackInvokedCallback="false"&lt;/code&gt; in your manifest as a &lt;strong&gt;temporary&lt;/strong&gt; escape hatch — but treat it as technical debt with a deadline. It will not be supported forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kotlin 2.4: Context Parameters Are Stable and They'll Change How You Write Compose Code
&lt;/h3&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%2Fophkdqxie1637dfoiofd.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%2Fophkdqxie1637dfoiofd.png" alt="Kotlin 2.4.0 release notes: context parameters are stable. No compiler flags required." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;&lt;em&gt;Kotlin 2.4.0 release notes: context parameters are stable. No compiler flags required.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;Kotlin 2.4.0, released in June 2026, promotes context parameters to stable. No compiler flags required. Production-ready today.&lt;/p&gt;

&lt;p&gt;Context parameters let a function declare that it needs a value to be "in scope" without the caller explicitly passing it. The compiler injects the dependency from the surrounding scope.&lt;/p&gt;

&lt;p&gt;Here is the problem they solve in a Compose codebase. Consider a common pattern — a function that needs both a &lt;code&gt;SnackbarHostState&lt;/code&gt; and a &lt;code&gt;CoroutineScope&lt;/code&gt; to show snackbar messages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The old way (lots of plumbing):&lt;/strong&gt;&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Explicitly plumbing SnackbarHostState and CoroutineScope parameters across multiple Compose layout functions (Antipattern).&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;&lt;strong&gt;The new way with context parameters:&lt;/strong&gt;&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Leveraging stable Kotlin 2.4 context parameters to eliminate ambient parameter drilling in Compose UI functions.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;The stronger use case: domain-level operations that need infrastructure — logging, analytics, navigation — without threading those concerns through every parameter.&lt;/p&gt;

&lt;p&gt;Context parameters work especially well with the NowInAndroid architecture pattern. In a repository that needs both a network client and a local DAO, context parameters let you declare those dependencies at the call site rather than injecting them into the constructor when the caller already has them in scope.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Declaring infrastructure dependencies at the call-site of a Repository using Kotlin 2.4 context parameters.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;This is not a replacement for Hilt or dependency injection. Context parameters solve a different problem: eliminating parameter drilling for values that are ambient within a call chain, not values that are injected at construction time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compose 1.11: Grid, Styles, and the Testing Overhaul
&lt;/h3&gt;

&lt;h4&gt;
  
  
  The Grid API
&lt;/h4&gt;

&lt;p&gt;Compose 1.11 ships a non-scrollable &lt;code&gt;Grid&lt;/code&gt; composable for building two-dimensional layouts. This is different from &lt;code&gt;LazyVerticalGrid&lt;/code&gt; — &lt;code&gt;Grid&lt;/code&gt; is for fixed-size layouts, not infinite lists.&lt;/p&gt;

&lt;p&gt;Before Grid, building a dashboard-style layout in Compose required either &lt;code&gt;LazyVerticalGrid&lt;/code&gt; (wrong — it's built for infinite content and has overhead for fixed layouts) or nested &lt;code&gt;Row&lt;/code&gt;/&lt;code&gt;Column&lt;/code&gt; composables (fragile and hard to maintain).&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Creating fixed-size dashboard UIs with the Compose 1.11 non-scrollable Grid API and GridItemSpan column spanning.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;The &lt;code&gt;GridItemSpan&lt;/code&gt; API gives you column-spanning without the SubcomposeLayout gymnastics that were previously required.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Styles API (Experimental)
&lt;/h4&gt;

&lt;p&gt;The experimental &lt;code&gt;Style {}&lt;/code&gt; API separates a component's visual appearance from its structural behavior. Instead of overriding &lt;code&gt;colors&lt;/code&gt;, &lt;code&gt;shapes&lt;/code&gt;, and &lt;code&gt;textStyle&lt;/code&gt; through individual parameters, you declare a style object that carries all visual customization.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Separating layout structure from visual customization using the experimental Compose 1.11 Style component API.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;The Styles API is still &lt;code&gt;@ExperimentalFoundationApi&lt;/code&gt; in 1.11. Do not ship it in production without understanding that the API surface may change. It is worth understanding now because this is the direction Compose is moving for design system work.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Testing API Overhaul
&lt;/h4&gt;

&lt;p&gt;Compose 1.11 makes the v2 testing APIs the default. This is a breaking change for test suites that relied on the implicit coroutine execution behavior of v1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changed:&lt;/strong&gt; v1 tests used &lt;code&gt;UnconfinedTestDispatcher&lt;/code&gt;, which ran coroutines immediately when launched. v2 uses &lt;code&gt;StandardTestDispatcher&lt;/code&gt;, which queues coroutines. They run only when you advance the virtual clock.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Broken asynchronous test pattern under Compose 1.11 due to queued StandardTestDispatcher behavior.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;The migration is mechanical: find every &lt;code&gt;runComposeUiTest&lt;/code&gt; block that asserts on asynchronous state and add &lt;code&gt;mainClock.advanceUntilIdle()&lt;/code&gt; or &lt;code&gt;mainClock.advanceTimeBy(timeMs)&lt;/code&gt; after &lt;code&gt;setContent&lt;/code&gt;. If you are using &lt;code&gt;waitUntil&lt;/code&gt;, review whether it is masking timing assumptions.&lt;/p&gt;

&lt;p&gt;The v1 APIs are deprecated but still present in 1.11. They will be removed in a future release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation 3 Is Stable. Time to Migrate.
&lt;/h3&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%2Fi82lcqdc7xzkr3jilm0a.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%2Fi82lcqdc7xzkr3jilm0a.png" alt="Navigation 3's NavDisplay handles back-gesture insets automatically. No manual inset wiring required." width="800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;&lt;em&gt;Navigation 3's `NavDisplay` handles back-gesture insets automatically. No manual inset wiring required.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;Jetpack Navigation 3 reached stable in late 2025 and is now the official navigation library for Compose apps. Navigation 2 (&lt;code&gt;androidx.navigation:navigation-compose&lt;/code&gt;) still works, but it will not receive new features.&lt;/p&gt;

&lt;p&gt;Navigation 3 does one thing Navigation 2 never could: it gives you full ownership of the back stack as state. The back stack is a &lt;code&gt;List&amp;lt;Any&amp;gt;&lt;/code&gt; that you hold in a &lt;code&gt;MutableList&lt;/code&gt; — a plain Kotlin value. Navigation observes it; it does not own it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration from Navigation 2 to Navigation 3:&lt;/strong&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;&lt;em&gt;Before:&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Application navigation routing structure using explicit NavHost string definitions in Jetpack Navigation 2.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;center&gt;&lt;small&gt;&lt;em&gt;After:&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Defining type-safe route keys and managing the backstack as observable state using Jetpack Navigation 3.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;The back stack is a &lt;code&gt;SnapshotStateList&amp;lt;NavKey&amp;gt;&lt;/code&gt;. You can read it, filter it, replace entries, and serialize it for process death restoration — none of which was possible with &lt;code&gt;NavController&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adaptive layouts with Navigation 3:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Navigation 3's architecture makes two-pane layouts straightforward. The same back stack drives both single-pane (phone) and two-pane (tablet/foldable) layouts:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Adapting a single navigation state backstack natively for single-pane (mobile) and dual-pane (tablet) Android devices.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;This pattern — a single back stack that adapts its display — is the official recommended approach for large-screen apps in 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Platform Goes Compose-First: Widgets, Camera, and Health Connect
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Jetpack Glance + RemoteCompose: Widgets Are Finally Good
&lt;/h4&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%2F7xl3ts186kliviqv2jfw.gif" 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%2F7xl3ts186kliviqv2jfw.gif" alt="The same Compose concepts that govern app UI now govern widget UI via Jetpack Glance + RemoteCompose." width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;&lt;em&gt;The same Compose concepts that govern app UI now govern widget UI via Jetpack Glance + RemoteCompose.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;Android 17 marks a shift toward a Compose-based development model for all widgets. The vehicle is RemoteCompose, integrated into Jetpack Glance.&lt;/p&gt;

&lt;p&gt;RemoteCompose renders full Compose animations on remote surfaces — home screen widgets and car displays. This means your home screen widget can have animated progress indicators, state transitions, and dynamic content without the RemoteViews limitations that made widget development miserable for a decade.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Building home screen widgets featuring live progress animations using Jetpack Glance and RemoteCompose.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;This is the first time &lt;code&gt;CircularProgressIndicator&lt;/code&gt; in a widget animates properly on the home screen without running a background service to push RemoteViews updates at a fixed interval.&lt;/p&gt;

&lt;h4&gt;
  
  
  Health Connect: FHIR Medical Records
&lt;/h4&gt;

&lt;p&gt;Health Connect in Android 16 adds write access for medical records in FHIR (Fast Healthcare Interoperability Resources) format. Health Connect is no longer just fitness data.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Saving Fast Healthcare Interoperability Resources (FHIR) medical documentation data inside Android 16 Health Connect.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;Apps that read FHIR records must declare &lt;code&gt;READ_MEDICAL_DATA_*&lt;/code&gt; permissions (scoped per category). The Health Connect permission dialog now shows users exactly what medical categories an app requests. This is a significant privacy surface — do not request medical permissions unless your app requires them.&lt;/p&gt;

&lt;h4&gt;
  
  
  CameraX: The Composable Viewfinder
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;CameraXViewfinder&lt;/code&gt; is now a stable &lt;code&gt;@Composable&lt;/code&gt; that replaces the old XML-only &lt;code&gt;PreviewView&lt;/code&gt;. It handles rotation, surface lifecycle, and camera configuration correctly across all window sizes.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;center&gt;&lt;small&gt;&lt;em&gt;Lifecycle-aware viewfinder rendering using CameraXViewfinder and a continuous capture controller overlay.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;The old pattern required wrapping &lt;code&gt;PreviewView&lt;/code&gt; in &lt;code&gt;AndroidView&lt;/code&gt;, which created lifecycle coordination issues when the composable was paused. &lt;code&gt;CameraXViewfinder&lt;/code&gt; handles this correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your Toolchain Changed Completely: Android Studio, CLI, and the Agentic Era
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Android Studio: Narwhal + I/O Edition
&lt;/h4&gt;

&lt;p&gt;The I/O edition of Android Studio ships a LeakCanary profiler task directly in the IDE. You no longer run LeakCanary as a separate tool — memory leak analysis runs as a first-class profiler task in the same panel as CPU and network profilers.&lt;/p&gt;

&lt;p&gt;Agent Mode in Gemini (available in Narwhal Feature Drop and later) handles multi-file tasks: generating tests for a feature, refactoring a module to a new architecture pattern, or migrating from Navigation 2 to Navigation 3. The agent formulates a plan, shows it to you, and executes only after you approve each step.&lt;/p&gt;

&lt;p&gt;The iOS app porting feature deserves special attention: you point Android Studio at an Xcode project and an AI agent analyzes the Swift/Objective-C code, maps it to equivalent Android patterns, and generates a working Kotlin/Compose app. Migrations that took weeks now take hours. The output is not perfect, but it produces a running app with the correct architecture patterns.&lt;/p&gt;

&lt;h4&gt;
  
  
  Android CLI: Your App Is Now Scriptable
&lt;/h4&gt;

&lt;p&gt;Android CLI (&lt;code&gt;adb&lt;/code&gt; replacement for semantic operations) is stable and exposes your app to AI agents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Semantic symbol resolution — ask Android CLI what a class does&lt;/span&gt;
android-cli symbols resolve &lt;span class="nt"&gt;--class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"com.example.HomeViewModel"&lt;/span&gt;
&lt;span class="c"&gt;# Render a Compose preview to file (for visual regression or AI analysis)&lt;/span&gt;
android-cli compose preview &lt;span class="nt"&gt;--composable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"HomeScreen"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;preview.png
&lt;span class="c"&gt;# Analyze a file for warnings without opening the IDE&lt;/span&gt;
android-cli analyze &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"app/src/main/java/com/example/HomeScreen.kt"&lt;/span&gt;
&lt;span class="c"&gt;# Run a specific UI test with Gemini-powered natural-language assertions&lt;/span&gt;
android-cli &lt;span class="nb"&gt;test &lt;/span&gt;journey &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Tap the login button, enter test@example.com and password123, verify the home screen shows"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;test journey&lt;/code&gt; command is the most significant: natural-language test assertions backed by Gemini's vision model. You describe the user flow in plain English; the CLI converts it into an automated test that runs against your app. This is how Google intends Android to participate in the agentic development toolchain.&lt;/p&gt;

&lt;h4&gt;
  
  
  Google AI Studio: Build Android Apps From a Prompt
&lt;/h4&gt;

&lt;p&gt;Google AI Studio now builds native Android apps directly. Select "Build an Android app," describe what you want, and the generated project uses Kotlin, Jetpack Compose, Material 3, and the current NowInAndroid architecture patterns. You can connect your Google Play developer account and publish directly to the Internal Test Track without leaving AI Studio.&lt;/p&gt;

&lt;p&gt;This matters for prototyping. The generated code is a starting point, not production code — but it generates the right starting point (Hilt, Room, MVVM, Compose Navigation) rather than the dated patterns that earlier code-generation tools produced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Android 17: What's Coming and the End of Developer Preview
&lt;/h3&gt;

&lt;p&gt;Android 17 is in its first beta as of Google I/O 2026. Two changes worth knowing now:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Canary channel replaces quarterly developer previews.&lt;/strong&gt; Android now ships new APIs and features to the Canary release of Android Studio as soon as they pass internal testing. You no longer wait for a scheduled Developer Preview drop. If you want to build against the latest Android platform APIs, stay on the Canary channel of Android Studio. This is the new normal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Widgets become Compose-native.&lt;/strong&gt; Android 17 completes the migration of Jetpack Glance to full RemoteCompose. The legacy RemoteViews approach still works, but Google will point all new widget development at Glance + Compose from Android 17 forward. If you have widgets written with RemoteViews, plan their migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pausable composition is now default.&lt;/strong&gt; Introduced experimentally in Compose 1.10, pausable composition lets the Compose runtime split long composition work across multiple frames — preventing jank on complex screens. It is enabled by default in Android 17's build of Compose. You do not need to change your code, but you should run your UI tests with the Compose 1.11 v2 testing APIs (see Section 3) to ensure they handle the non-immediate execution correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your Action Items
&lt;/h3&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%2F0mzcds09ccbdha5ubst4.gif" 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%2F0mzcds09ccbdha5ubst4.gif" alt="A correctly handled edge-to-edge bottom bar: scrim for three-button navigation, transparent for gesture navigation. This is what your app should look like on Android 16." width="760" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;center&gt;&lt;small&gt;&lt;em&gt;A correctly handled edge-to-edge bottom bar: scrim for three-button navigation, transparent for gesture navigation. This is what your app should look like on Android 16.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;p&gt;The most urgent changes are in this order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ship this week:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add &lt;code&gt;enableEdgeToEdge()&lt;/code&gt; to every &lt;code&gt;Activity&lt;/code&gt; and remove all &lt;code&gt;WindowCompat.setDecorFitsSystemWindows&lt;/code&gt; calls&lt;/li&gt;
&lt;li&gt;Audit every &lt;code&gt;onBackPressed&lt;/code&gt; override — migrate to &lt;code&gt;BackHandler&lt;/code&gt; or &lt;code&gt;PredictiveBackHandler&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run your Compose tests with Compose 1.11 — any test that fails on &lt;code&gt;StandardTestDispatcher&lt;/code&gt; has a hidden timing assumption&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Ship this month:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Migrate from Navigation 2 to Navigation 3, Upgrade to Kotlin 2.4 and audit your codebase for context parameter opportunities. Test your app on the Android 16 emulator with gesture navigation enabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan for next quarter:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Migrate widgets from RemoteViews to Jetpack Glance + RemoteCompose 8. Evaluate CameraX replacement if you use &lt;code&gt;PreviewView&lt;/code&gt; in &lt;code&gt;AndroidView&lt;/code&gt; Explore Health Connect FHIR if your app touches health data. Enable Android Studio Agent Mode for your most repetitive architectural tasks (generating tests, migrating modules)&lt;/p&gt;

&lt;h4&gt;
  
  
  What Google I/O 2026 Actually Means
&lt;/h4&gt;

&lt;p&gt;Android in 2026 is not the same platform it was in 2021 when Compose shipped. The ecosystem has converged. Kotlin is the only language. Compose is the only UI toolkit. MVVM with Hilt and Flow is the official architecture. Navigation 3 gives you back-stack as state. The toolchain generates apps that follow these patterns by default.&lt;/p&gt;

&lt;p&gt;Five years of investment in Compose is paying off at the platform level: widgets, car displays, Wear OS tiles, and TV all converge on the same Compose model. The XML layout editor, &lt;code&gt;Fragment&lt;/code&gt;s, &lt;code&gt;ViewPager2&lt;/code&gt; — these are now legacy APIs, maintained but not advanced.&lt;/p&gt;

&lt;p&gt;The developers who win in this cycle are the ones who use today's stack to its depth — not the ones who maintain the widest possible backward compatibility. Android 16's mandatory edge-to-edge and killed &lt;code&gt;onBackPressed&lt;/code&gt; are not aggressive changes. They are the platform finally enforcing what it recommended for two years.&lt;/p&gt;

&lt;p&gt;Your users already expect apps to feel like part of the OS. Now the OS enforces it.&lt;/p&gt;

&lt;center&gt;&lt;small&gt;&lt;em&gt;All code in this article targets Android 16 (API 36), Kotlin 2.4, Compose 1.11, Navigation 3, and uses the NowInAndroid architecture patterns with Hilt and StateFlow. The minimum supported SDK in examples is API 24.&lt;/em&gt;&lt;/small&gt;&lt;/center&gt;

&lt;h4&gt;
  
  
  Sources
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://android-developers.googleblog.com/2026/05/17-things-android-developers-google-io.html" rel="noopener noreferrer"&gt;Android Developers Blog: 17 Things to know for Android developers at Google I/O&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://android-developers.googleblog.com/2026/04/jetpack-compose-april-2026-updates.html" rel="noopener noreferrer"&gt;Android Developers Blog: What's new in the Jetpack Compose April '26 release&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://android-developers.googleblog.com/2025/11/jetpack-navigation-3-is-stable.html" rel="noopener noreferrer"&gt;Jetpack Navigation 3 is stable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/about/versions/16/summary" rel="noopener noreferrer"&gt;Android 16 features and changes list&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/about/versions/16/behavior-changes-16" rel="noopener noreferrer"&gt;Behavior changes: Apps targeting Android 16 or higher&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kotlinlang.org/docs/whatsnew24.html" rel="noopener noreferrer"&gt;What's new in Kotlin 2.4.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://android-developers.googleblog.com/2026/05/whats-new-android-developer-tools.html" rel="noopener noreferrer"&gt;Android Developers Blog: Android Studio I/O Edition: What's new in Android Developer tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doveletter.dev/release-notes/google-io-2026-android" rel="noopener noreferrer"&gt;Everything About Google I/O 2026 for Android Developers | Dove Letter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://android-developers.googleblog.com/2026/02/the-first-beta-of-android-17.html" rel="noopener noreferrer"&gt;The First Beta of Android 17&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://9to5google.com/2026/05/19/google-ai-studio-android-apps/" rel="noopener noreferrer"&gt;Google AI Studio can now build Android apps&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>androiddevelopment</category>
      <category>kotlin</category>
      <category>kotlincoroutines</category>
      <category>jetpackcompose</category>
    </item>
  </channel>
</rss>
