<?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: Ethan Zhang</title>
    <description>The latest articles on DEV Community by Ethan Zhang (@ethan-zhang-dev).</description>
    <link>https://dev.to/ethan-zhang-dev</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%2F3893744%2F1720bdcc-3126-4d83-95b0-63c0868269f1.png</url>
      <title>DEV Community: Ethan Zhang</title>
      <link>https://dev.to/ethan-zhang-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ethan-zhang-dev"/>
    <language>en</language>
    <item>
      <title>How to analyze duplicate processing in an async flow</title>
      <dc:creator>Ethan Zhang</dc:creator>
      <pubDate>Mon, 29 Jun 2026 04:42:49 +0000</pubDate>
      <link>https://dev.to/ethan-zhang-dev/how-to-analyze-duplicate-processing-in-an-async-flow-510j</link>
      <guid>https://dev.to/ethan-zhang-dev/how-to-analyze-duplicate-processing-in-an-async-flow-510j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;In one line: deduplication is about the &lt;strong&gt;evidence that a side effect has been applied&lt;/strong&gt; — make it atomic with the effect, visible to everyone, and tied to a unique identifier.&lt;/p&gt;

&lt;p&gt;This is just how I think it out — not a tutorial, not the final answer. I'm sharing my reasoning, and I'd love to hear where it breaks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What comes to mind when you're asked about a duplicate issue?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;maybe we can use the Inbox pattern to solve it.&lt;/li&gt;
&lt;li&gt;maybe we can use Redis SETNX to build a distributed lock.&lt;/li&gt;
&lt;li&gt;maybe ...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wait a minute pls. Let's step back on this topic and not get stuck in some tech solutions.&lt;/p&gt;

&lt;p&gt;So, my questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What does duplicate processing mean, at its core?&lt;/li&gt;
&lt;li&gt;What conditions cause it to happen?&lt;/li&gt;
&lt;li&gt;Which boundaries together guarantee dedup?&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;By the way, I tried to enumerate all the failure scenarios the typical implementations aim to prevent, but I gave up — there are too many possibilities to list them all. So I'll start from the essence of duplicate processing instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is duplicate processing, exactly?
&lt;/h2&gt;

&lt;p&gt;So, what is duplicate processing at its core?&lt;/p&gt;

&lt;p&gt;It's not about how many times a message is &lt;strong&gt;delivered&lt;/strong&gt;. It's about how many times the &lt;strong&gt;side effect&lt;/strong&gt; is &lt;strong&gt;applied&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delivered N times but the side effect applied once → correct.&lt;/li&gt;
&lt;li&gt;Delivered N times, the side effect applied multiple times but idempotent (harmless) → correct, but wasteful.&lt;/li&gt;
&lt;li&gt;Delivered N times, the side effect applied more than once and it's not idempotent → that's the real problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So at its root: &lt;strong&gt;duplicate processing means the same logical intent has its side effect applied more than once.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And one more thing: even when duplication happens, it only causes damage if the side effect is &lt;strong&gt;not&lt;/strong&gt; idempotent. An idempotent side effect makes a duplicate harmless — but still wasteful, and real business logic is often hard to make idempotent. So idempotency isn't our goal here; the discussion below does not assume it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What conditions cause it to happen?
&lt;/h2&gt;

&lt;p&gt;Now, instead of jumping to solutions, let's think the other way around: &lt;strong&gt;under what conditions does the side effect get applied more than once?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is what I think — it happens if any of these is true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identifier:&lt;/strong&gt; The same logical intent has &lt;strong&gt;no unique identifier&lt;/strong&gt;, so two arrivals are treated as two different things.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomicity:&lt;/strong&gt; The side effect runs, but &lt;strong&gt;the evidence that it was applied is not written atomically with it&lt;/strong&gt; — so after a crash or a failed commit, the system can't tell it was applied, and runs it again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visibility:&lt;/strong&gt; That evidence lives only in &lt;strong&gt;one consumer's local memory&lt;/strong&gt; — so after a rebalance, a restart, or across worker threads, whoever picks the message up next can't see it, and runs it again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice something: each condition above is just a way the guarantee breaks. So if we &lt;strong&gt;flip them around&lt;/strong&gt;, we get the boundaries that guarantee non-duplication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which boundaries together guarantee dedup?
&lt;/h2&gt;

&lt;p&gt;Flipping the failure conditions, here are the boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;unique identifier&lt;/strong&gt; per logical intent, tied to the same business side effect.&lt;/li&gt;
&lt;li&gt;The side effect and its &lt;strong&gt;evidence record&lt;/strong&gt; — the proof that the side effect for this identifier has been applied — must be written &lt;strong&gt;atomically&lt;/strong&gt;: both succeed or both fail, never just one. (Eventual consistency is needed if the side effect crosses an external system.)&lt;/li&gt;
&lt;li&gt;The evidence record must be &lt;strong&gt;visible to every processor&lt;/strong&gt; — not living in any single consumer's memory — so it stays visible across rebalances, restarts, and worker threads, and remains valid for as long as a duplicate could still occur. (Whether it lives in Redis or a relational table is just an implementation choice — the essence is that the evidence exists, with the necessary information.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After that, we can decide what the orchestrator and collaborators look like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collaborators:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Evidence Checker&lt;/strong&gt; — checks the evidence record to see whether the side effect for this identifier has already been applied. The record is visible to every processor, independent of any single consumer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Side-effect Handler&lt;/strong&gt; — applies the side effect and writes the evidence record atomically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offset Committer&lt;/strong&gt; — confirms to the MQ that the message has been consumed. (No need to tie this to a specific MQ here — that's an implementation trade-off.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Orchestrator:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConsumerHandler&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;EvidenceChecker&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;SideEffectHandler&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;OffsetCommitter&lt;/span&gt; &lt;span class="n"&gt;committer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;consume&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CommitHandle&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(...);&lt;/span&gt;

        &lt;span class="c1"&gt;// Has the side effect for this identifier already been applied?&lt;/span&gt;
        &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;alreadyApplied&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;identifierKey&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="c1"&gt;// Already applied — skip the work, just commit and return.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alreadyApplied&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;committer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;commit&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;handle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Within the same atomic boundary:&lt;/span&gt;
            &lt;span class="c1"&gt;// 1. apply the side effect (business logic)&lt;/span&gt;
            &lt;span class="c1"&gt;// 2. write the evidence record for this identifier&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;

        &lt;span class="n"&gt;committer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;commit&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Take care of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We haven't discussed any concrete tech (RDBMS, Redis) yet.&lt;/li&gt;
&lt;li&gt;The early &lt;code&gt;alreadyApplied&lt;/code&gt; check is a &lt;strong&gt;performance optimization, not a correctness guarantee&lt;/strong&gt;. Even with an idempotent side effect, reprocessing a duplicate still wastes resources — CPU, DB calls, external requests — so the check lets us skip that work and return fast. But it does NOT prevent duplication itself: a check-then-act still has a race window. The real guarantee comes from the &lt;strong&gt;unique constraint&lt;/strong&gt; when the evidence record is written atomically.&lt;/li&gt;
&lt;li&gt;No matter what MQ we use (Kafka, RabbitMQ, or something else), the consumer always needs the &lt;strong&gt;message&lt;/strong&gt; and a &lt;strong&gt;way to commit/confirm&lt;/strong&gt; it — otherwise it can't know the message was consumed. That's why &lt;code&gt;consume(Message message, CommitHandle handle)&lt;/code&gt; is written like this.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  So, what's left?
&lt;/h2&gt;

&lt;p&gt;So far everything is still &lt;strong&gt;technology-agnostic&lt;/strong&gt; — we went from the essence, to the failure conditions, to the boundaries, and finally to an abstract collaboration model. No Redis, no RDBMS yet.&lt;/p&gt;

&lt;p&gt;The abstract model is clean. Reality usually isn't. The &lt;code&gt;handler.handle(...)&lt;/code&gt; above still treats business logic as a black box — and that box might not be simple. When the side effect is more than one step, what does its evidence record look like then?&lt;/p&gt;

&lt;p&gt;So I'll leave it here as a question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What problems do you think are still hiding? What would you have to design or reason about next?&lt;/strong&gt; And if you'd leave any comment to help refine this post, feel free to let me know — thanks in advance.&lt;/p&gt;

&lt;p&gt;The point of this post: &lt;strong&gt;find the boundaries first, and every later solution has a place to fit.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>discuss</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>When code becomes cheaper, what still makes an engineer valuable?</title>
      <dc:creator>Ethan Zhang</dc:creator>
      <pubDate>Fri, 12 Jun 2026 09:56:24 +0000</pubDate>
      <link>https://dev.to/ethan-zhang-dev/when-code-becomes-cheaper-what-still-makes-an-engineer-valuable-24hm</link>
      <guid>https://dev.to/ethan-zhang-dev/when-code-becomes-cheaper-what-still-makes-an-engineer-valuable-24hm</guid>
      <description>&lt;p&gt;&lt;strong&gt;When code becomes cheaper, what still makes an engineer valuable?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recently, while writing my cover letter for remote roles and Upwork projects, I asked myself a very direct question:&lt;/p&gt;

&lt;p&gt;Why should a remote team or client choose me, especially in the AI era?&lt;/p&gt;

&lt;p&gt;I do not think the answer should be: “Because I am the strongest engineer technically.”&lt;/p&gt;

&lt;p&gt;That is not how I want to position myself.&lt;/p&gt;

&lt;p&gt;What I want to become is this:&lt;/p&gt;

&lt;p&gt;A backend engineer who can turn unclear business problems into reliable, maintainable systems.&lt;/p&gt;

&lt;p&gt;AI is making implementation faster. It can generate code, explain technologies, and provide alternatives. At the same time, remote work and platforms like Upwork make competition more global. We are not only competing with engineers nearby, but also with engineers from everywhere.&lt;/p&gt;

&lt;p&gt;If the only question is “Who knows more frameworks, patterns, or tools?”, many ordinary engineers may feel hopeless.&lt;/p&gt;

&lt;p&gt;But I believe there is another path.&lt;/p&gt;

&lt;p&gt;In real systems, code is only part of the work. Someone still needs to understand the business workflow. Someone still needs to define what “correct” means. Someone still needs to identify risks, edge cases, performance concerns, and reliability boundaries.&lt;/p&gt;

&lt;p&gt;My usual way of working starts from these questions:&lt;/p&gt;

&lt;p&gt;What is the real requirement?&lt;br&gt;
What does correctness mean in this workflow?&lt;br&gt;
What data must stay consistent?&lt;br&gt;
What edge cases could break the process?&lt;br&gt;
What performance or reliability signals should be protected?&lt;br&gt;
Where should the module boundary be?&lt;br&gt;
Who should orchestrate the main flow, and who should act as collaborators?&lt;/p&gt;

&lt;p&gt;This “orchestrator + collaborators” thinking helps me keep the main business process clear. The orchestrator owns the workflow. The collaborators handle specific responsibilities such as validation, translation, persistence, messaging, or external integration.&lt;/p&gt;

&lt;p&gt;I also use AI in this process, but not only to generate code. I use it to challenge my assumptions, explore alternatives, find missing cases, improve naming, review design trade-offs, and refine the implementation. AI is an accelerator, not a replacement for engineering judgment.&lt;/p&gt;

&lt;p&gt;One example is a definition-driven batch import/export module I worked on for a return-order system.&lt;/p&gt;

&lt;p&gt;At first, it looked like a normal Excel import/export feature. But the real challenge was not Excel itself. The business template changed over time, and import and export had to stay aligned with the same rules.&lt;/p&gt;

&lt;p&gt;So I designed the module around template definitions instead of hard-coded columns. The import flow included raw validation, translation, business validation, and database update. The export flow reused the same definition model to keep the output consistent with the import contract.&lt;/p&gt;

&lt;p&gt;This made the system easier to reason about: where the data came from, which rule failed, which cell caused the problem, and how future template changes should be handled.&lt;/p&gt;

&lt;p&gt;That is the kind of work I care about.&lt;/p&gt;

&lt;p&gt;So I keep asking myself:&lt;/p&gt;

&lt;p&gt;In the AI era, do remote teams and Upwork clients only need someone who can write code faster?&lt;/p&gt;

&lt;p&gt;Or do they also need someone who can understand the problem, reduce uncertainty, communicate clearly, and take responsibility for the result?&lt;/p&gt;

&lt;p&gt;I do not know if this makes me the best engineer.&lt;/p&gt;

&lt;p&gt;But this is the kind of engineer I want to keep becoming:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A backend engineer who turns unclear business problems into reliable, maintainable systems, using AI as an accelerator while still owning correctness, judgment, and delivery.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>backendengineering</category>
      <category>java</category>
      <category>remotework</category>
      <category>aiera</category>
    </item>
  </channel>
</rss>
