<?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: Suprie</title>
    <description>The latest articles on DEV Community by Suprie (@suprie_32).</description>
    <link>https://dev.to/suprie_32</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%2F349066%2Fbe319537-6589-4014-93f5-ee6b59509992.jpeg</url>
      <title>DEV Community: Suprie</title>
      <link>https://dev.to/suprie_32</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/suprie_32"/>
    <language>en</language>
    <item>
      <title>The NUMMI Blueprint for AI: Why Code Velocity Needs the Andon Cord</title>
      <dc:creator>Suprie</dc:creator>
      <pubDate>Sat, 10 Oct 2026 00:01:07 +0000</pubDate>
      <link>https://dev.to/suprie_32/the-nummi-blueprint-for-ai-why-code-velocity-needs-the-andon-cord-1hk0</link>
      <guid>https://dev.to/suprie_32/the-nummi-blueprint-for-ai-why-code-velocity-needs-the-andon-cord-1hk0</guid>
      <description>&lt;p&gt;I first heard about NUMMI years ago on an episode of &lt;em&gt;This American Life&lt;/em&gt;. Toyota took over one of General Motors’ worst factories in Fremont, California, rehired many of the same workers, and turned it into one of its best plants. As software engineering enters the era of AI-generated code, NUMMI offers a crucial lesson: increasing speed without built-in mechanisms for quality only helps you produce defects faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  One of GM’s Worst Factories
&lt;/h2&gt;

&lt;p&gt;In the early 1980s, GM closed its Fremont plant due to chronic absenteeism, grievances, strikes, and deliberate sabotage. The prevailing view was simple: &lt;strong&gt;the workers were the problem.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;When Toyota created the NUMMI joint venture in 1984, they rehired those exact same workers and sent union leaders to Japan to study the Toyota Production System. They learned that quality was not enforced by end-of-line inspectors—it was built into every step by the people doing the work. The workers returned and transformed GM’s worst plant into a model of efficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cord and the Flow
&lt;/h2&gt;

&lt;p&gt;At the heart of NUMMI’s success was the &lt;em&gt;andon cord&lt;/em&gt;. If a worker found a defect, they pulled a cord to call for help, stopping the line if necessary. At GM, workers were forced to let defects move downstream, where they became exponentially more expensive to fix. Toyota viewed a defect moving downstream as a failure, treating problem discovery not as an error, but as an opportunity to fix the system.&lt;/p&gt;

&lt;p&gt;Modern software engineering digitized this concept:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;commit → build → test → deploy → production&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CI/CD, automated testing, feature flags, and instant rollbacks are simply digital andon cords designed around &lt;strong&gt;flow&lt;/strong&gt;—producing in small batches to get immediate feedback and contain damage early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kaizen and Culture
&lt;/h2&gt;

&lt;p&gt;Toyota also practiced &lt;em&gt;kaizen&lt;/em&gt; (continuous improvement) by listening to the factory floor. They built simple fixtures and tools to eliminate awkward movements, recognizing that the person doing the job daily understands friction better than management. In software, we call this developer experience: internal platforms, faster CI builds, and local setup scripts.&lt;/p&gt;

&lt;p&gt;NUMMI demonstrated that ordinary people inside a good system produce extraordinary results, while talented people trapped in a bad system eventually look incompetent. But culture cannot be a motivational poster. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Culture is not what leadership writes on the wall.&lt;br&gt;&lt;br&gt;
Culture is what the system rewards when something goes wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Now We Have AI
&lt;/h2&gt;

&lt;p&gt;AI has made generating software fast and cheap. However, using AI purely to maximize volume leads directly to &lt;strong&gt;AI slop&lt;/strong&gt;: unmaintainable code, superficial test coverage, hallucinated documentation, and over-engineered abstractions.&lt;/p&gt;

&lt;p&gt;AI makes bad engineering practices scale just as quickly as good ones. If AI generates code ten times faster, our feedback loops must adapt. Automated tests, strict observability, clean architecture, and human comprehension become more critical, not less. The goal should not be &lt;em&gt;"How much more code can we generate?"&lt;/em&gt; but rather &lt;em&gt;"How can AI remove friction so humans can think better?"&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Andon Cord
&lt;/h2&gt;

&lt;p&gt;Because AI agents can generate fifty "stations" of work before a human steps in, we need explicit circuit breakers to stop runaway generation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests fail?&lt;/strong&gt; Stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope creeps beyond the task?&lt;/strong&gt; Stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The diff exceeds readable limits?&lt;/strong&gt; Stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture grows unnecessarily complex?&lt;/strong&gt; Stop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The faster we generate, the more valuable the ability to stop becomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don’t Ask AI to Move the Line Faster
&lt;/h2&gt;

&lt;p&gt;Engineering leaders must resist measuring AI solely through output metrics like lines of code or closed tickets. Go to where the work happens: sit with engineers, identify where AI adds friction, and build guardrails around it. &lt;/p&gt;

&lt;p&gt;Without the right system, AI simply speeds up the production of bad software. With the right system—and an andon cord built for AI—it becomes the ultimate tool in an engineer's hands.&lt;/p&gt;

</description>
      <category>techculture</category>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>leansoftware</category>
    </item>
    <item>
      <title>Idempotency Part 2: The Backend Contract - Why ON CONFLICT Is Not Enough</title>
      <dc:creator>Suprie</dc:creator>
      <pubDate>Sat, 03 Oct 2026 16:27:17 +0000</pubDate>
      <link>https://dev.to/suprie_32/idempotency-part-2-the-backend-contract-why-on-conflict-is-not-enough-473i</link>
      <guid>https://dev.to/suprie_32/idempotency-part-2-the-backend-contract-why-on-conflict-is-not-enough-473i</guid>
      <description>&lt;h2&gt;
  
  
  The Real Problem Is Not Duplicate Requests
&lt;/h2&gt;

&lt;p&gt;The naive backend implementation looks safe until two requests arrive at the same time. Both check whether an idempotency key exists. Both see nothing. Both create an order.&lt;/p&gt;

&lt;p&gt;That is a time-of-check to time-of-use race. Checking the key in application code does not reserve it.&lt;/p&gt;

&lt;p&gt;Postgres can arbitrate that race with a primary key on &lt;code&gt;(user_id, key)&lt;/code&gt; and &lt;code&gt;INSERT ... ON CONFLICT DO NOTHING&lt;/code&gt;. Among competing transactions that successfully commit, only one can claim that key.&lt;/p&gt;

&lt;p&gt;But winning the claim is only the beginning.&lt;/p&gt;

&lt;p&gt;If you commit the key and then create the order in a separate transaction, the process can crash between those steps. The key says &lt;code&gt;processing&lt;/code&gt;, but there is no order to process. Every retry finds the same unfinished claim.&lt;/p&gt;

&lt;p&gt;The fix is to commit the idempotency record and the order together. That protects order creation. Protecting what happens outside Postgres requires another guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  High-Level Architecture
&lt;/h2&gt;

&lt;p&gt;The flow starts with four components. The iOS client sends an order with an &lt;code&gt;Idempotency-Key&lt;/code&gt; header. The Rust API opens a Postgres transaction, claims the key, creates the order, and links the two records. It commits before calling the matching engine.&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%2Fxhn80u8zhqt9pwtrd1tb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhn80u8zhqt9pwtrd1tb.png" alt="Architecture showing the client, API, Postgres, and matching engine" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Postgres holds the durable state for both the original request and its retries. Redis is unnecessary for correctness in this design.&lt;/p&gt;

&lt;p&gt;If the API crashes before commit, neither record becomes committed, and a retry can try again. If it crashes after commit but before calling the matching engine, the order remains &lt;code&gt;pending&lt;/code&gt;, and a background reconciler can recover it.&lt;/p&gt;

&lt;p&gt;There is one more failure window: the matching engine accepts the order, but the API crashes before recording that result. Postgres still says &lt;code&gt;pending&lt;/code&gt;. A reconciler might submit the order again.&lt;/p&gt;

&lt;p&gt;To make that recovery safe, the matching engine must recognize a repeated submission using a stable identifier, such as the order ID. The database transaction prevents duplicate order creation; downstream deduplication prevents duplicate execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the Table to Store Intent, Not Just Keys
&lt;/h2&gt;

&lt;p&gt;An idempotency record needs to describe the request and its outcome. A useful schema includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A primary key on &lt;code&gt;(user_id, key)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A request fingerprint for detecting changed payloads.&lt;/li&gt;
&lt;li&gt;The order ID created for that request.&lt;/li&gt;
&lt;li&gt;A processing status.&lt;/li&gt;
&lt;li&gt;The response status and body, once available.&lt;/li&gt;
&lt;li&gt;An expiration timestamp.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the table serves multiple operations, include the operation in the key scope or explicitly require keys to be unique across those operations.&lt;/p&gt;

&lt;p&gt;Define the fingerprint deliberately. Hashing raw JSON bytes treats changes in whitespace or field order as different requests. For an order API, hashing a normalized representation of the validated order fields may better capture the intent.&lt;/p&gt;

&lt;p&gt;Persisting the response lets retries replay the original outcome without rebuilding it from an order whose state may have changed. Storing JSON in &lt;code&gt;jsonb&lt;/code&gt; preserves its content, but does not promise the original byte representation.&lt;/p&gt;

&lt;p&gt;The IETF Idempotency-Key draft recommends returning the result of a previously completed operation. It also recommends &lt;code&gt;422&lt;/code&gt; when the same key is reused with a different payload and &lt;code&gt;409&lt;/code&gt; when the original operation is still outstanding. These are draft recommendations, rather than a requirement for byte-identical response replay.&lt;/p&gt;

&lt;h2&gt;
  
  
  ACID in Practice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Atomicity: Claim and Order Must Commit Together
&lt;/h3&gt;

&lt;p&gt;Begin a transaction and try to insert the idempotency record.&lt;/p&gt;

&lt;p&gt;If the insert succeeds, create the order and link it to the claim within that same transaction. Commit both records together.&lt;/p&gt;

&lt;p&gt;If the insert does not create a row, end that transaction and read the existing record in a fresh transaction. A competing insert can wait for the winning transaction to finish, so a conflict does not necessarily mean that the entire business operation has completed.&lt;/p&gt;

&lt;p&gt;The existing record determines the next step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A different fingerprint returns &lt;code&gt;422&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A completed operation returns its stored result.&lt;/li&gt;
&lt;li&gt;An outstanding operation follows the API’s documented concurrent-request behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this design, that last case returns &lt;code&gt;409&lt;/code&gt;. An asynchronous API can instead define order acceptance as the completed operation, persist a &lt;code&gt;202&lt;/code&gt; acceptance response alongside the order, and expose matching progress through a separate endpoint.&lt;/p&gt;

&lt;p&gt;The important point is to choose a clear contract. “Order accepted” and “order matched” are different outcomes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consistency: A Key Identifies One Intent
&lt;/h3&gt;

&lt;p&gt;Reusing the same key for a different payload must not silently return an unrelated order.&lt;/p&gt;

&lt;p&gt;Store the fingerprint when claiming the key and compare it on every duplicate request. A view-model bug that reuses a key for a different symbol should produce an explicit error.&lt;/p&gt;

&lt;p&gt;The idempotency key does not replace business constraints. Balance checks, valid order transitions, and other invariants still need their own enforcement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isolation: Let the Unique Index Arbitrate the Claim
&lt;/h3&gt;

&lt;p&gt;Do not rely on &lt;code&gt;SELECT&lt;/code&gt; followed by &lt;code&gt;INSERT&lt;/code&gt; to decide who owns the request. Both transactions can observe that the key is absent.&lt;/p&gt;

&lt;p&gt;Use the unique constraint to arbitrate ownership. Under PostgreSQL’s default Read Committed isolation, retrieve the existing record with a subsequent read after the conflicting insert finishes.&lt;/p&gt;

&lt;p&gt;For later state transitions, use conditional updates or row locks where necessary. If both the API and a reconciler can process pending work, they also need coordination to avoid unnecessary concurrent submissions.&lt;/p&gt;

&lt;p&gt;That coordination reduces overlap. The matching engine’s deduplication guarantee still handles retries after uncertain outcomes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Durability: Persist the Outcome Before Responding
&lt;/h3&gt;

&lt;p&gt;After the matching engine returns a definitive result, update the order and idempotency record together in a second transaction. Store the response, commit, and then respond to the client.&lt;/p&gt;

&lt;p&gt;If the API crashes after that commit but before the client receives the response, a retry can retrieve the stored outcome.&lt;/p&gt;

&lt;p&gt;If it crashes before recording the result, the original claim and order still exist. The retry does not need to create another order. Recovery must resolve the existing order’s outcome, either by querying the matching engine or by safely resubmitting the same order ID.&lt;/p&gt;

&lt;p&gt;This guarantee assumes Postgres is configured for durable commits. ACID protects the records in Postgres; it cannot make an external call part of the same transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking Through the Crash Windows
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure point&lt;/th&gt;
&lt;th&gt;Durable state&lt;/th&gt;
&lt;th&gt;Recovery&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Before the first commit&lt;/td&gt;
&lt;td&gt;No committed claim or order&lt;/td&gt;
&lt;td&gt;Retry can claim and create&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After commit, before submission&lt;/td&gt;
&lt;td&gt;Claim and pending order&lt;/td&gt;
&lt;td&gt;Reconciler submits the existing order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After engine acceptance, before recording the result&lt;/td&gt;
&lt;td&gt;Claim and apparently pending order&lt;/td&gt;
&lt;td&gt;Query the engine or resubmit with downstream deduplication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After recording the result, before responding&lt;/td&gt;
&lt;td&gt;Completed record and stored response&lt;/td&gt;
&lt;td&gt;Retry replays the result&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third row is the easy one to miss. A timeout or crash does not tell you whether the external operation happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Long to Keep the Key
&lt;/h2&gt;

&lt;p&gt;Retention is part of the API contract.&lt;/p&gt;

&lt;p&gt;Twenty-four hours might suit some order workflows. Other systems may need several days or longer. Choose the window based on client retries, offline behavior, recovery time, and the consequences of replaying an old request.&lt;/p&gt;

&lt;p&gt;Deleting a key ends its deduplication protection. A late retry may then look like a new request.&lt;/p&gt;

&lt;p&gt;Publish the expiration policy, and avoid deleting unresolved records merely because their original retry window has elapsed.&lt;/p&gt;

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

&lt;p&gt;Mobile taught me to distrust the network. Backend taught me to distrust my own process crashing between two lines of code.&lt;/p&gt;

&lt;p&gt;Idempotency is where those two distrusts meet.&lt;/p&gt;

&lt;p&gt;A transaction makes the request’s intent durable. A stored response makes the outcome repeatable. Once execution crosses a service boundary, the receiving system must recognize that same intent too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next: Where COMMIT Is Not Enough — The Transactional Outbox
&lt;/h2&gt;

&lt;p&gt;The diagram still contains a gap: we commit the order, then call the matching engine.&lt;/p&gt;

&lt;p&gt;A reconciler can recover pending orders, but we can represent the intent to publish explicitly. Instead of relying on an immediate call after commit, insert an outbox event in the same transaction as the order and idempotency claim.&lt;/p&gt;

&lt;p&gt;The transaction now commits three things together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request’s idempotency record.&lt;/li&gt;
&lt;li&gt;The order.&lt;/li&gt;
&lt;li&gt;The durable intent to publish that order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A separate relay reads committed outbox events, publishes them, and marks them as sent.&lt;/p&gt;

&lt;p&gt;This moves the &lt;strong&gt;intent to publish&lt;/strong&gt; inside the database transaction. The external publication still happens outside it.&lt;/p&gt;

&lt;p&gt;If the relay publishes successfully and crashes before marking the event as sent, it will publish that event again. The outbox therefore supports at-least-once delivery; consumers must deduplicate using a stable event or order ID. Ordering also requires an explicit design.&lt;/p&gt;

&lt;p&gt;In Part 3, I will implement the Postgres outbox and a Rust relay. I will use &lt;code&gt;LISTEN/NOTIFY&lt;/code&gt; to wake the relay when work arrives, with startup and reconnect scans plus periodic recovery checks for outstanding work.&lt;/p&gt;

&lt;p&gt;Notifications are a signal to look for work. The outbox table is the durable record of that work.&lt;/p&gt;

&lt;p&gt;Then I will carry the same order ID through delivery and matching, so replaying an outbox event cannot execute the order twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header" rel="noopener noreferrer"&gt;IETF Idempotency-Key draft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/14/transaction-iso.html" rel="noopener noreferrer"&gt;PostgreSQL transaction isolation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/current/sql-listen.html" rel="noopener noreferrer"&gt;PostgreSQL LISTEN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://microservices.io/patterns/data/transactional-outbox" rel="noopener noreferrer"&gt;Transactional outbox pattern&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>postgres</category>
    </item>
    <item>
      <title>When Cheap isn't cheap anymore.</title>
      <dc:creator>Suprie</dc:creator>
      <pubDate>Sun, 27 Sep 2026 10:53:24 +0000</pubDate>
      <link>https://dev.to/suprie_32/when-cheap-isnt-cheap-anymore-1o36</link>
      <guid>https://dev.to/suprie_32/when-cheap-isnt-cheap-anymore-1o36</guid>
      <description>&lt;p&gt;During my time in my previous company, I've learned a lot, but here are two: (1) New APIs aren't always the right fit - diffable data source is great for partial updates, not full replacements. (2) 'Cheap' operations aren't cheap at scale - profile before you assume.&lt;/p&gt;

&lt;p&gt;I've been tasked with handling real-time data using web sockets. On paper, the feature was easy. Instead of stopping at 10 price levels, show all of them - which could be anywhere between 80 to 137 price levels depending on the stock. During peak hours, the view that was responsive suddenly became frozen. Users couldn't scroll up and down, and unfortunately, this happened only during peak trading hours, so it slipped through QA.&lt;/p&gt;

&lt;p&gt;I thought Diffable Data Source would fix it. I implemented it based on data associated with &lt;code&gt;UUID&lt;/code&gt;, but it just made things worse. The issue is that the data sent isn't a snapshot of changes but the entire list of price levels. Diffing the entire list every time was pure overhead - no partial updates to optimize for. In this case, using &lt;code&gt;tableView.reloadData()&lt;/code&gt; is faster.&lt;/p&gt;

&lt;p&gt;Then I fired up the profiler to find the real bottleneck. Looking through the results, I was quite surprised. The computed properties - like color for up and down, and the number formatting - were clogging the main thread. The computations were cheap, but when hit with 30 packages per second, each carrying all price levels, they weren't cheap anymore. Even the localization we take for granted can clog up the main thread.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Before: 5 levels × 30 updates/sec × (formatting + color calc + localization) = manageable&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After: 100 levels × 30 updates/sec × same operations = UI death&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution is relatively straightforward: no more calculations on computed properties. All properties are calculated in background threads when converting the WebSocket response into view data. The view becomes dumb - it just reads values from the view data. After the changes, it managed to survive peak trading hours with smooth scrolling. &lt;/p&gt;

&lt;p&gt;Now when we hit performance issues, we know where to look first.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>programming</category>
      <category>software</category>
    </item>
    <item>
      <title>Idempotency: Protecting User Intent Beyond the Buy Button</title>
      <dc:creator>Suprie</dc:creator>
      <pubDate>Fri, 11 Sep 2026 09:03:36 +0000</pubDate>
      <link>https://dev.to/suprie_32/idempotency-protecting-user-intent-beyond-the-buy-button-l63</link>
      <guid>https://dev.to/suprie_32/idempotency-protecting-user-intent-beyond-the-buy-button-l63</guid>
      <description>&lt;p&gt;User bought some stock but the network is slow. There is no immediate feedback, so out of frustration, they tap the &lt;strong&gt;Buy&lt;/strong&gt; button several times. A few seconds later, multiple orders are matched. &lt;/p&gt;

&lt;p&gt;Have you ever got into this situation? &lt;/p&gt;

&lt;p&gt;Disabling the button in the UI helps, but it isn't guarantee. UI state changes happen on main thread and depend on when the run loops get a chance to process them. If the main thread is busy, multiple tap events may be handled before the UI visibly transitions into its disabled state. &lt;/p&gt;

&lt;p&gt;And even when the UI behaves perfectly, relying on the client alone is dangerous. This is where &lt;strong&gt;idempotency&lt;/strong&gt; comes in. &lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency
&lt;/h2&gt;

&lt;p&gt;From &lt;strong&gt;&lt;strong&gt;Designing Data Intensive Applications&lt;/strong&gt;&lt;/strong&gt;: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An idempotent operation is one that you can perform multiple times, and it has the same effect as if you performed it only once. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In our case, what we actually want to protect is the &lt;strong&gt;user's intents&lt;/strong&gt;. If the user intends to buy stock once, we don't want multiple taps, retries, or network issues to accidentally turn that into multiple orders. &lt;/p&gt;

&lt;p&gt;But isn't that the backend jobs? Yes, but not entirely. The client needs a way to tell the server: "These requests represent the same intent", while the server needs a way to recognize that intent and ensure it is processed only once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Idempotency is an end-to-end contract, not just a backend trick&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So Who creates the idempotent key and when?&lt;/p&gt;

&lt;p&gt;In our case, since we wanted to protect the user intent, it's on the client side. And since it's purpose to protect the user intent, I'm not recommend doing something like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UUID&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;This defeats the purpose. A key is created every time. Instead, I would do something along this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;OrderDTO&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Side&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Decimal&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;lot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;@MainActor&lt;/span&gt;
&lt;span class="kd"&gt;@Observable&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;OrderFormViewModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Decimal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;lot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; 
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Side&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buy&lt;/span&gt; 

    &lt;span class="kd"&gt;private(set)&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;pendingOrder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;OrderDTO&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;OrderDTO&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;pendingOrder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pendingOrder&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;newOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;pendingOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newOrder&lt;/span&gt;
            &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newOrder&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; 

        &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;orderService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;orderService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;pendingOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// retry using the pending order&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;OrderDTO&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;OrderDTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; 
            &lt;span class="nv"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="nv"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="nv"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;lot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lot&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important is not the UUID itself. It is when that UUID is created and how long we keep it. Once user submits the order, we create an order intent and assign idempotencyKey to it. Any retry with the same intent will reuse the same key. &lt;/p&gt;

&lt;p&gt;So even if the user spams the buy button, it will create the order once and then send it to server. The key should only change when the user creates a new intent. For example, after the previous order has completed and they decided to create another one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you wanted to more orderly fashion instead of using UUID, which use UUID version 4, you can find some third pary library that generate UUID version 7 which include timestamp &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But generating the key on the client is half of the contract. The server still needs to decide what to do when it receives the same key more than once. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;POST /order/submit
Idempotency-Key: 01DA...

POST /order/submit
Idempotency-Key: 01DA...

POST /order/submit
Idempotency-Key: 01DA...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server should not treated them as three orders. Instead it can store the relationship between the idempotency key and the order being processed&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;01DA... -&amp;gt; Order #1234 -&amp;gt; Pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the request arrived with the same key, the server recognized that it has already seen this intent. Rather than creating another order, it can return the existing result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if the order has already completed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1234"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"matched"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the responsibility is quite obvious:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The client declares the intent, and the server guarantees that the intent is not executed twice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that is where things get more interesting on the backend: where do we store the key? How long should it live? What happens if two identical requests arrive at exactly the same time?&lt;/p&gt;

&lt;p&gt;That's a topic for the next post.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>idempotency</category>
      <category>ios</category>
    </item>
    <item>
      <title>The Sneaky Coordinator Leak in UIViewRepresentable</title>
      <dc:creator>Suprie</dc:creator>
      <pubDate>Mon, 07 Sep 2026 23:36:02 +0000</pubDate>
      <link>https://dev.to/suprie_32/the-sneaky-coordinator-leak-in-uiviewrepresentable-h0p</link>
      <guid>https://dev.to/suprie_32/the-sneaky-coordinator-leak-in-uiviewrepresentable-h0p</guid>
      <description>&lt;p&gt;Let's talk a little about a memory leak I recently found while working with UIViewRepresentable.&lt;br&gt;
The issue happened in a custom UIKit view wrapped inside SwiftUI. At first, nothing looked suspicious. The view worked fine. But after navigating in and out of the screen a few times, the Memory Graph Debugger showed that the Coordinator instances kept increasing.&lt;br&gt;
Here is a simplified version of the setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;SwiftUI&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;UIKit&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;CallbackButton&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIButton&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;onTap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CGRect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;addTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;#selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handleTap&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touchUpInside&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;?(&lt;/span&gt;&lt;span class="nv"&gt;coder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSCoder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;fatalError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"init(coder:) has not been implemented"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;@objc&lt;/span&gt; &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;handleTap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;onTap&lt;/span&gt;&lt;span class="p"&gt;?()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;deinit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CallbackButton deinit"&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="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;LeakyCallbackButtonView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIViewRepresentable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;@Binding&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;makeCoordinator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Coordinator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;Coordinator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;makeUIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;CallbackButton&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CallbackButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// Looks harmless, but this strongly captures the coordinator.&lt;/span&gt;
        &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onTap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handleTap&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;updateUIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;uiView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CallbackButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;uiView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Coordinator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;@Binding&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
        &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Binding&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Coordinator init"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;handleTap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Tapped at &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;deinit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Coordinator deinit"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suspicious line is this one:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;button.onTap = context.coordinator.handleTap&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It looks like we are only passing a function, but an instance method reference still needs to retain its instance. In this case, the closure stored button.onTap strongly retains the Coordinator.&lt;br&gt;
In a normal UIKit view controller, many people would immediately fix this with &lt;code&gt;[weak self]&lt;/code&gt;. But this is &lt;code&gt;UIViewRepresentable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UIViewRepresentable&lt;/code&gt; is a struct, so self is a value type. You cannot use &lt;code&gt;[weak self]&lt;/code&gt; the same way you would inside a &lt;code&gt;UIViewController&lt;/code&gt;, because weak only applies to class-bound references.&lt;/p&gt;

&lt;p&gt;The fix is to weakly capture the Coordinator instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;FixedCallbackButtonView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIViewRepresentable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;@Binding&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;makeCoordinator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Coordinator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;Coordinator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;makeUIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;CallbackButton&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CallbackButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onTap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="n"&gt;coordinator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleTap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;updateUIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;uiView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CallbackButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;uiView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;dismantleUIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;uiView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CallbackButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;coordinator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Coordinator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;uiView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onTap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Coordinator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;@Binding&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
        &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Binding&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Coordinator init"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;handleTap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Tapped at &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;deinit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Coordinator deinit"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two fixes here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onTap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="n"&gt;coordinator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handleTap&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;This prevents the closure from strongly retaining the Coordinator.&lt;br&gt;
And:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;dismantleUIView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;uiView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CallbackButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;coordinator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Coordinator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;uiView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onTap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This clears the callback when SwiftUI dismantles the UIKit view.&lt;br&gt;
My takeaway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// UIKit / UIViewController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// UIViewRepresentable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="n"&gt;coordinator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="n"&gt;coordinator&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doSomething&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;UIViewRepresentable is a value-type wrapper, but the Coordinator is the reference object that usually carries UIKit-style lifetime and callback behavior. So when a UIKit view stores a closure, be careful not to accidentally retain the Coordinator through that closure.&lt;/p&gt;

&lt;p&gt;Just a note for myself.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>ios</category>
      <category>mobile</category>
      <category>swift</category>
    </item>
    <item>
      <title>MVVM vs VIP, which one is the best?</title>
      <dc:creator>Suprie</dc:creator>
      <pubDate>Thu, 03 Sep 2026 02:17:28 +0000</pubDate>
      <link>https://dev.to/suprie_32/mvvm-vs-vip-which-one-is-the-best-1b0k</link>
      <guid>https://dev.to/suprie_32/mvvm-vs-vip-which-one-is-the-best-1b0k</guid>
      <description>&lt;p&gt;The short answer is neither.&lt;/p&gt;

&lt;p&gt;There is no silver bullet; no single architecture can fit all codebases. And code, like requirements, is never fixed. It evolves alongside the codebase. What should not drift accidentally are boundaries.&lt;/p&gt;

&lt;p&gt;For a greenfield project, I'd start with MVVM. Not because MVVM is inherently better than VIP, VIPER, or any other architecture, but because it's simple enough to start with, many developers are familiar with it, it works naturally with SwiftUI's state-driven model, and if done well, keeps the ViewModel focused on presentation state.&lt;/p&gt;

&lt;p&gt;The mistake was thinking that an architecture's name will protect the codebase for you. It won't. A ViewModel can still become a dumping ground for network, persistence, and business rules. If infrastructure details leak into the Interactor or ViewModel, or dependencies are not hidden behind appropriate adapters, the code becomes harder to isolate, replace, and test.&lt;/p&gt;

&lt;p&gt;Here's what a bad Interactor looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;LoginInteractor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;LoginBusinessLogic&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;displayLogic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;LoginDisplayLogic&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;doLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://api.example.com/login"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URLRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;httpMethod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"POST"&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;URLSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;JSONDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;displayLogic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showLoginSuccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;displayLogic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showLoginFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDescription&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;LoginInteractorTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;@Test&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;testDoLogin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;displayLogic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;FakeLoginDisplayLogic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;interactor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;LoginInteractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;displayLogic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;displayLogic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;interactor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"abc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"abc"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// doLogin fires a Task internally — there's no way to await it from here.&lt;/span&gt;
        &lt;span class="c1"&gt;// We can't inject a fake result, so this test either:&lt;/span&gt;
        &lt;span class="c1"&gt;//   1. Hits the real endpoint with real credentials, or&lt;/span&gt;
        &lt;span class="c1"&gt;//   2. Races the network call and asserts before it even completes.&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// guessing how long the network needs&lt;/span&gt;
        &lt;span class="cp"&gt;#expect(displayLogic.isLoggedIn == true) // flaky: fails if the network is slow&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;Compare that to an Interactor that depends on an abstraction instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="kt"&gt;LoginWorker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;doLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;LoginResult&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;LoginInteractor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;LoginBusinessLogic&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;displayLogic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;LoginDisplayLogic&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;LoginWorker&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;doLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;displayLogic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showLoginSuccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;displayLogic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showLoginFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDescription&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;LoginInteractorTest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;@Test&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;testDoLogin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;displayLogic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;FakeLoginDisplayLogic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;// Now you can just mock the worker, make it throw any error like succeed,&lt;/span&gt;
        &lt;span class="c1"&gt;// ,failed, or timeout without need to wait for estimated time api call succeed&lt;/span&gt;
        &lt;span class="c1"&gt;// because it never happened&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;FakeLoginWorker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;interactor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;LoginInteractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;displayLogic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;displayLogic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;interactor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"abc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"abc"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="cp"&gt;#expect(displayLogic.isLoggedIn == true)&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;The architecture name matters less than whether the boundaries are actually respected.&lt;/p&gt;

&lt;p&gt;The goal was never to reach a specific architecture, the goal is always to make the code understandable as complexity grows.&lt;/p&gt;

&lt;p&gt;Dependency direction still matters:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Presentation -&amp;gt; Application / Use Case -&amp;gt; Domain &amp;lt;- Data Infrastructure&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That direction defines the boundaries. UI should not know how data is fetched. Business rules should not care whether the data comes from an API, cache, or local DB. Infrastructure should not dictate how the application works.&lt;/p&gt;

&lt;p&gt;Those boundaries are more important than whether a type is called ViewModel, Presenter, Interactor, or UseCase. If developers repeatedly struggle to answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where does this behavior belong?&lt;/li&gt;
&lt;li&gt;Why does changing this feature require touching unrelated code?&lt;/li&gt;
&lt;li&gt;Why do I need to understand five layers to change one screen?&lt;/li&gt;
&lt;li&gt;What else breaks if I change this?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the current architecture may no longer be doing its job. That's a signal to refactor — not because of hype, but because the existing structure is no longer helping the team reason about change.&lt;/p&gt;

&lt;p&gt;For an existing codebase, I'd optimize for incremental boundary improvements rather than introducing a new architecture everywhere. Create seams and the Strangler Fig pattern. The goal is understanding the system, not adopting any particular architecture pattern. Refactoring an existing codebase without understanding it first is like entering the battlefield without joining the briefing. You might know how to fight, but you don't yet know the objective, the terrain, the constraints, or why the current position exists.&lt;/p&gt;

&lt;p&gt;Ugly code sometimes contains important business knowledge, or a workaround from a production incident years ago. Duplication may be accidental, or it may represent behavior that diverged for a reason. Before replacing the architecture, understand the system.&lt;/p&gt;

&lt;p&gt;For a legacy codebase, the process is closer to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Understand -&amp;gt; Test -&amp;gt; Find seam -&amp;gt; Replace -&amp;gt; Reassess&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;rather than&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Choose architecture -&amp;gt; Rewrite&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Testability is another important part of this. Architectures like MVVM and VIP often trade some delivery speed for clearer responsibilities, predictability, and easier testing. But testability only matters if you actually make use of it. Architecture can tell you where responsibilities belong. Tests tell you what the system is expected to do. That distinction becomes critical over time. Three months after a feature ships, someone will start wondering: "Is this intentional, or a bug?" A good dependency graph can't answer that, but more often than not, a good test can.&lt;/p&gt;

&lt;p&gt;Consistency helps make reviews, debugging, onboarding, and refactoring easier. But it's not the goal by itself. The goal is a system the team can understand and change safely.&lt;/p&gt;

&lt;p&gt;The best architecture is not the one with the most rules. It's the lightest structure that preserves your system's boundaries, and still lets the team change the code with confidence.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>architecture</category>
      <category>mvvm</category>
      <category>vip</category>
    </item>
  </channel>
</rss>
