<?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: taewoo</title>
    <description>The latest articles on DEV Community by taewoo (@taewoo).</description>
    <link>https://dev.to/taewoo</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%2F4093271%2Fda7bd8a9-657f-44c0-b7bc-21a93280e395.jpeg</url>
      <title>DEV Community: taewoo</title>
      <link>https://dev.to/taewoo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/taewoo"/>
    <language>en</language>
    <item>
      <title>19 days, 546 messages, 141 users: building my app by mostly talking to Claude Code</title>
      <dc:creator>taewoo</dc:creator>
      <pubDate>Sat, 24 Oct 2026 06:00:59 +0000</pubDate>
      <link>https://dev.to/taewoo/19-days-546-messages-141-users-building-my-app-by-mostly-talking-to-claude-code-1mgb</link>
      <guid>https://dev.to/taewoo/19-days-546-messages-141-users-building-my-app-by-mostly-talking-to-claude-code-1mgb</guid>
      <description>&lt;p&gt;I make Tabby, a free tab manager for Chrome, Firefox and Edge. This week I shipped version 1.2 and made a "lock in" short for TikTok and YouTube in three languages.&lt;/p&gt;

&lt;p&gt;I barely wrote code for any of it. Mostly I talked to Claude Code in plain Vietnamese ("this looks ugly", "the text is cut off", "the music is wrong"), looked at what it showed me, and said yes or no.&lt;/p&gt;

&lt;p&gt;People keep asking me three things: how long it takes, whether it actually works, and what it costs. So here are my real numbers, plus the times it misunderstood me.&lt;/p&gt;

&lt;p&gt;🎬 1.2 video: &lt;a href="https://youtu.be/rFEa6yeJnX8" rel="noopener noreferrer"&gt;https://youtu.be/rFEa6yeJnX8&lt;/a&gt;&lt;br&gt;
🎬 The short: &lt;a href="https://youtube.com/shorts/m5bQXdLr7zc" rel="noopener noreferrer"&gt;English&lt;/a&gt; · &lt;a href="https://youtube.com/shorts/Ziiuu_yBlHo" rel="noopener noreferrer"&gt;日本語&lt;/a&gt; · &lt;a href="https://youtube.com/shorts/8ZetyyQyl4Q" rel="noopener noreferrer"&gt;中文&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What got done
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tabby 1.2:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;29 themes, 6 of them free (Graphite, Catppuccin, Dracula, Zen Garden, Sakura, Capy Chill)&lt;/li&gt;
&lt;li&gt;12 languages&lt;/li&gt;
&lt;li&gt;a calmer board and a new Home page&lt;/li&gt;
&lt;li&gt;new store pages in every language&lt;/li&gt;
&lt;li&gt;a 51-second promo video&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The "lock in" short:&lt;/strong&gt; someone fills a year of work into Tabby, one line at a time, cut to the beat. It ships in English, Japanese and Chinese, each in a different theme.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long, and how much talking
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;My messages&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;Cost at API prices*&lt;/th&gt;
&lt;th&gt;Versions thrown away&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tabby 1.2, plan to release&lt;/td&gt;
&lt;td&gt;about 1 day (Oct 22 night → Oct 23 evening)&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;888M&lt;/td&gt;
&lt;td&gt;~$316&lt;/td&gt;
&lt;td&gt;0 (fixed in review rounds)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lock-in short: plan, 3 versions, 2 more languages, posting&lt;/td&gt;
&lt;td&gt;about 3.5 hours&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;207M&lt;/td&gt;
&lt;td&gt;~$55&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The whole project so far (since Oct 5)&lt;/td&gt;
&lt;td&gt;19 days&lt;/td&gt;
&lt;td&gt;546&lt;/td&gt;
&lt;td&gt;2.5B&lt;/td&gt;
&lt;td&gt;~$909&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;* I worked this out from the token counts in Claude Code's logs, priced at Opus API list prices. I'm on Claude Max, a flat monthly plan, so this isn't what I paid. It's what the same work would cost through the API. Most of those tokens are Claude re-reading the project, which is billed at the cheap cached rate.&lt;/p&gt;

&lt;p&gt;For scale, 58 messages is about one long chat with a friend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out of it (so far)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;141 users&lt;/strong&gt; on the Chrome and Firefox stores, 19 days after the first line of code. Rating 5.0 from 3 reviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3.4K views and 6.7 hours of watch time&lt;/strong&gt; on YouTube.&lt;/li&gt;
&lt;li&gt;About &lt;strong&gt;$6 of API-priced AI time per user&lt;/strong&gt; so far ($909 ÷ 141).&lt;/li&gt;
&lt;li&gt;On the short, &lt;strong&gt;one message from me every ~5 minutes&lt;/strong&gt; (45 in 3.5 hours). Claude did the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's early, and these are small numbers. But this is one person, and the "team" is a chat window.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I actually work with it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I say what I want like I'd say it to a person.&lt;/strong&gt; "Make a version in Japanese and Chinese, different themes, and a more positive hook." No specs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It plans first, and I approve the plan.&lt;/strong&gt; Early on it changed my website before I'd seen the plan. Now there's a rule in its memory file: show the plan, wait for my OK, then build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It shows me, not tells me.&lt;/strong&gt; Screenshots, a 2-second demo clip, a page with the options drawn side by side. I can judge a picture in five seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It reviews itself before I see it.&lt;/strong&gt; For the big parts, I let a second Claude review the first one's work and score it. They went back and forth until it hit 9–10/10. One night I said "review it to 10/10, then build it" and went to sleep. In the morning it was done.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where it got me wrong, and how we found out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. "The fonts don't match the design."&lt;/strong&gt;&lt;br&gt;
I noticed every theme looked slightly off. Claude compared each screen against the design, one by one, and found that the app's font had never loaded. Not in any theme, not since day one. Chrome's own default style was quietly winning.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who found it:&lt;/strong&gt; I did, by looking. All the automatic tests had passed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; one rule, the same morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; tests check that things work, not that they look right. Look at it yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. A button was cut off in Vietnamese.&lt;/strong&gt;&lt;br&gt;
It looked fine in English. In Vietnamese the third button fell off the edge.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who found it:&lt;/strong&gt; I did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Claude fixed the button. It also added a check that opens every screen at phone width in all 12 languages and fails if anything sticks out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; one bug report becomes a test, so the same bug can't come back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. The short: it made an ad. I wanted a vibe.&lt;/strong&gt;&lt;br&gt;
I showed it a trending TikTok. Its first two versions filmed the whole app, with lots of lines popping in at once. They were accurate, and nobody could read them on a phone.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why it got it wrong:&lt;/strong&gt; it was trying to show off the product. The trend only works when you see one line being typed, close up, and the app stays in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How I fixed it:&lt;/strong&gt; I sent one screenshot of a video with 7k likes and said "like this, never the whole dashboard". I also asked it to write that down as a rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; version 3 got it right, and the Japanese and Chinese versions needed no changes from me.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. The music was the wrong song.&lt;/strong&gt;&lt;br&gt;
It found the trending sound on TikTok. That turned out to be a sped-up re-upload, not the original.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who found it:&lt;/strong&gt; I did. It just sounded off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; I sent YouTube links to the originals. It re-measured the beat and moved every cut onto the new beats. One version still had the drop 0.2 s early, and Claude caught that itself on review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5. It said too much.&lt;/strong&gt;&lt;br&gt;
An early cut had a caption: "so I locked in". I said: show, don't tell. Now the only words on screen are a browser tab title at the start and the link at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone trying this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your job becomes taste and saying no.&lt;/strong&gt; Most of my 58 messages were "no, because…" or "yes, do it".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it show you a small demo before it builds the big thing.&lt;/strong&gt; A 2-second clip saves an hour of wrong renders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When it gets something wrong, ask it to write the lesson down.&lt;/strong&gt; Then you only have to say it once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look at the result yourself.&lt;/strong&gt; Every important bug here was found by a human looking, not by a test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count the cost in your time, not only in dollars.&lt;/strong&gt; 103 messages from me covered a release and four videos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tabby is free with no account: &lt;a href="https://tabby.taewoo.me/?s=devto" rel="noopener noreferrer"&gt;https://tabby.taewoo.me/?s=devto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you work with an AI agent too, how do you stop it from building the wrong thing?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Payment Integrations in Two Days with Claude Code</title>
      <dc:creator>taewoo</dc:creator>
      <pubDate>Wed, 21 Oct 2026 17:10:53 +0000</pubDate>
      <link>https://dev.to/taewoo/payment-integrations-in-two-days-with-claude-code-4aka</link>
      <guid>https://dev.to/taewoo/payment-integrations-in-two-days-with-claude-code-4aka</guid>
      <description>&lt;p&gt;Adding a new payment provider used to take me about a week. Over the last five integrations at a payments company, it took one to two days each, and reviewers found fewer missing cases. The AI didn't get smarter in between. I changed the order of the work, and I stopped letting the agent that wrote the code also judge it.&lt;/p&gt;

&lt;p&gt;This post is for engineers who integrate third-party APIs, payments or not, and wonder how far to trust an AI coding agent. You'll get the five steps I use, the rule that decides what to build first, and the one bias that makes AI review useless if you don't break it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The week was never spent on typing
&lt;/h2&gt;

&lt;p&gt;Most of an integration week goes into reading and checking, not writing code.&lt;/p&gt;

&lt;p&gt;Think of a restaurant taking on a new supplier. Cooking is quick. The slow part is learning the supplier's catalogue, deciding what happens when a delivery is late or wrong, and making sure nobody pays for the same crate twice.&lt;/p&gt;

&lt;p&gt;A payment integration is the same: read the provider's docs, map their payment statuses to yours, list what can go wrong (duplicate webhooks, unknown statuses, timeouts), write the code and tests, then explain it all to a reviewer who has never seen this provider.&lt;/p&gt;

&lt;p&gt;Before you read on, guess which step I now spend the &lt;em&gt;most&lt;/em&gt; time on. It's the first one, before any code exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five steps: script, fixtures, build, fresh review, check
&lt;/h2&gt;

&lt;p&gt;The rule: I decide what "correct" means before the agent starts, and a different agent checks the result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[UPLOAD IMAGE HERE: /Users/taewoo/hustle/profiles/x-images/01-workflow-5-steps.png]&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Script (me).&lt;/strong&gt; I write the scenarios and the &lt;strong&gt;goals&lt;/strong&gt;: concrete checks the finished code must pass. Each goal calls the code and compares the result against a fixture, or calls the provider's sandbox when there is one. I also rank the risks low, medium and high. High is anything that loses or doubles money: status mapping, the idempotency key (the ID that makes a repeated request safe to apply twice), and whether polling stops at the right moment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fixtures (Claude Code).&lt;/strong&gt; A &lt;strong&gt;fixture&lt;/strong&gt; is a saved example response from the provider. If the provider has a sandbox, the agent calls it and saves the real JSON; I can use that same JSON to test before integrating anything. If there's no sandbox, the agent fetches the docs and builds fixtures from them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build (Claude Code).&lt;/strong&gt; High-risk logic gets written first, while everyone is still paying attention. Familiar parts just follow our existing conventions. The phase isn't done until every goal passes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review (a fresh agent).&lt;/strong&gt; A new session gets only the diff and the goals. More on why below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check (me).&lt;/strong&gt; A skill exports two things: a map of every critical decision in the code, and JSON requests so I can re-test the API in one pass. I read the map, not the whole diff.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Engineers call these reusable instructions &lt;strong&gt;skills&lt;/strong&gt;: saved recipes Claude Code loads for one kind of task. The risk map in step 5 is one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide the scary cases before the agent does
&lt;/h2&gt;

&lt;p&gt;The most valuable rules are the ones written before any code: what happens when something unexpected arrives.&lt;/p&gt;

&lt;p&gt;My favourite example: &lt;strong&gt;an unknown status becomes &lt;code&gt;pending&lt;/code&gt;, plus an alert.&lt;/strong&gt; Payment providers add statuses without telling you. If the code guesses, it might mark an unpaid order as paid, or a paid order as failed. Parking it as pending loses nothing, and the alert means a human looks at it the same day.&lt;/p&gt;

&lt;p&gt;I learned this one from a code review, not from an AI. The new code met a status it didn't recognise, raised an error, caught it and carried on. That looked harmless. But further down, a shared rule said: if a payment's status hasn't changed by the end of processing, mark it failed. So every unknown status quietly became a failed payment. Payments that might have gone through were recorded as failed, the success rate dropped, and nobody could tell why, because "failed" hid the real cause. The fix was to stop guessing: park it as pending, alert the team, and let a human check.&lt;/p&gt;

&lt;p&gt;An AI agent will make the same kind of choice, and happily: it invents a reasonable-looking default. Reasonable-looking is exactly the problem, because nobody questions it in review. Writing the rule into the script first means the agent implements my decision instead of making its own.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  For engineers: what a risk list in the script looks like
  &lt;p&gt;A trimmed example of the shape (not a real provider):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Risks&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; HIGH  status mapping: unknown status → pending + alert, never paid/failed
&lt;span class="p"&gt;-&lt;/span&gt; HIGH  idempotency key: one key per payout, reused on every retry
&lt;span class="p"&gt;-&lt;/span&gt; HIGH  polling: stop on a final status or after N attempts, then alert
&lt;span class="p"&gt;-&lt;/span&gt; MED   amount format: provider sends minor units (×100)
&lt;span class="p"&gt;-&lt;/span&gt; LOW   response field names: follow existing adapter conventions

&lt;span class="gu"&gt;## Goals&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; webhook "SUCCESS" fixture → payment paid, exactly once on replay
&lt;span class="p"&gt;-&lt;/span&gt; webhook with unknown status → payment pending, alert sent
&lt;span class="p"&gt;-&lt;/span&gt; payout retried after timeout → provider sees the same idempotency key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each goal is a test that runs against a fixture, or against the sandbox when there is one.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent that wrote the code shouldn't review it
&lt;/h2&gt;

&lt;p&gt;Ask the same session to review its own work and it mostly agrees with itself. It remembers why it made each choice, so it reads the code through those reasons.&lt;/p&gt;

&lt;p&gt;So review is a new agent with no memory of the build. It gets two things: the diff and the goals. It has to work out from scratch whether the code does what the goals say. In my experience this catches gaps well, because it reads the code the way a new teammate would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the agent still goes wrong
&lt;/h2&gt;

&lt;p&gt;Left alone, the agent tends to build more than you need, and "more" can quietly delete what you had.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The override that skipped the safety checks.&lt;/strong&gt; The project already had a shared base class for provider adapters, and the method we needed was already there. The agent created a subclass anyway and overrode that method for no real reason. The override skipped checks the base class had collected from earlier incidents. Every new test passed, because the tests only covered the new provider. That's why my script now says "familiar parts follow conventions" and why the risk map lists every override.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lists that jumped and drifted.&lt;/strong&gt; In Tabby, my browser extension, I asked the agent to render only the rows on screen, so the dashboard stays fast with 5,000 saved links. It worked: the dashboard went from about 4 seconds to 0.14 seconds. But the generated code started at the wrong scroll position and counted one margin twice, so rows slowly drifted. The end-to-end tests caught both.&lt;/p&gt;

&lt;p&gt;Both bugs passed the agent's own checks. Both were caught by something it didn't control: a review that knew the history, or a test written beforehand.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs, and the rules I keep
&lt;/h2&gt;

&lt;p&gt;The speed comes with an upfront bill: the script and goals take real time, and skipping them is how you lose money.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Money paths get human eyes:&lt;/strong&gt; signature checks, amount conversion, idempotency and any database migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No secrets or customer data in prompts.&lt;/strong&gt; Sandbox keys only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Goals before code.&lt;/strong&gt; If I can't say what correct means, the agent can't either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't let the agent over-engineer.&lt;/strong&gt; Reuse what exists; an unexplained override is a review flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs-based fixtures are a guess.&lt;/strong&gt; Without a sandbox, the exported JSON is how I check the guess against the real API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The alternative is writing it all by hand. That's the right call for a one-off integration with no repeated pattern, or when you can't review the result properly. It's the wrong call when a new provider arrives every few weeks and the same risks apply each time.&lt;/p&gt;

&lt;p&gt;A note on the numbers: "about a week" and "1–2 days" are my own before-and-after for five integrations at work, from starting the ticket to merging the PR. They're not a controlled experiment, and the providers differed in complexity.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Write the goals and the risky rules (like "unknown status → pending + alert") before the agent writes code.&lt;/li&gt;
&lt;li&gt;Build high-risk logic first; let familiar parts follow existing conventions.&lt;/li&gt;
&lt;li&gt;Review with a fresh agent that sees only the diff and the goals, never the session that wrote the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you integrate payment providers and do it differently, or think I've got a rule wrong, email me at &lt;a href="mailto:tainh.work@gmail.com"&gt;tainh.work@gmail.com&lt;/a&gt;. I'll correct the post.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://taewoo.me/posts/payment-integrations-with-claude-code/" rel="noopener noreferrer"&gt;taewoo.me&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>fintech</category>
    </item>
    <item>
      <title>I force-quit Chrome with 29 tabs open. Chrome restored 0. Tabby restored all 29.</title>
      <dc:creator>taewoo</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:48:04 +0000</pubDate>
      <link>https://dev.to/taewoo/what-went-wrong-in-my-tab-managers-first-release-and-how-11-fixes-it-22im</link>
      <guid>https://dev.to/taewoo/what-went-wrong-in-my-tab-managers-first-release-and-how-11-fixes-it-22im</guid>
      <description>&lt;p&gt;I had 29 tabs open. I force-quit Chrome instantly (a &lt;code&gt;SIGKILL&lt;/code&gt;, the same as a real crash), with no chance to save anything. Chrome sometimes offers "Restore" after a crash. This time, when I reopened it, it restored nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tabby brought back all 29 tabs in one click.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's because Tabby keeps its own copy of your open windows, on your device, and updates it as your tabs change. You don't have to remember to save anything.&lt;/p&gt;

&lt;p&gt;Tabby is a free tab manager I made for Chrome and Firefox, and version 1.1 is out today. Getting here meant fixing three launch-week mistakes. The worst one: just having Gmail open quietly stopped Tabby's quick backups, because of a timer trap any app can fall into.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/9ev2-RG53NA" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Free, no account. Coming from Toby? Your collections import in one click. Unlike OneTab, it saves without you clicking:&lt;/em&gt; &lt;strong&gt;&lt;a href="https://chromewebstore.google.com/detail/tabby/olgjlfcacknhhlgcclkicjafbiaogjnc" rel="noopener noreferrer"&gt;Add to Chrome&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href="https://addons.mozilla.org/firefox/addon/tabby-tab-manager/" rel="noopener noreferrer"&gt;Add to Firefox&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I got wrong in launch week
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. With Gmail open, the quick backup never ran
&lt;/h3&gt;

&lt;p&gt;No tabs were lost. But for an app whose one promise is &lt;em&gt;you won't lose anything&lt;/em&gt;, this one hurt.&lt;/p&gt;

&lt;p&gt;Tabby takes a quick backup about 30 seconds after your tabs change. Gmail changes its tab title every time mail arrives: "(3) Inbox", then "(4) Inbox". Tabby counted each title change as "your tabs changed" and restarted the 30-second wait. With Gmail open, the wait never ended. Slack and other chat apps did the same. Only the slower 10-minute backup kept running.&lt;/p&gt;

&lt;p&gt;The cause is a quiet Chrome detail: setting a timer with a name that's already taken &lt;strong&gt;replaces&lt;/strong&gt; the old one, deadline and all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: every tab change replaced the pending timer and pushed it back.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alarms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;snapshot-soon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;delayInMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// After: only start a timer if none is already waiting.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Title, favicon and audio changes also no longer count as "tabs changed". A test now opens a page whose title changes every second and checks that the quick backup still happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; a timer that restarts on every event never fires if the events never stop. That's a &lt;em&gt;debounce&lt;/em&gt; with no maximum wait. A backup needs a deadline that new events can't push back: start the timer once, and let later changes join it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. I came back after a break and couldn't use my own app
&lt;/h3&gt;

&lt;p&gt;1.0 explained its features but never showed how to use them. So 1.1 adds a &lt;strong&gt;one-minute hands-on tour&lt;/strong&gt; in a practice browser, so your real tabs are never touched. It points at one thing to click at a time and tells you clearly when you've done it.&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/.%2Fimg%2Ftour.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/.%2Fimg%2Ftour.png" alt="The tour: a checklist on the left, a practice browser with a " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; explaining a feature isn't the same as showing someone where to click.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Google rejected the update over five words
&lt;/h3&gt;

&lt;p&gt;My store description named five of the new color themes: Tokyo Night, Rosé Pine, Gruvbox, Aurora and Linen. Those names belong to other open-source projects, and Google flagged the line as "keyword stuffing". One plain line, &lt;em&gt;"Eight color themes, light or dark"&lt;/em&gt;, fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson for extension authors:&lt;/strong&gt; keep other projects' names out of your store text and screenshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new in 1.1, in 30 seconds
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bring it back.&lt;/strong&gt; This is how the 29 tabs came back. Closed a window by mistake, or crashed? Closed windows are kept for 14 days. One click restores them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close tabs without fear.&lt;/strong&gt; Save a tab to &lt;strong&gt;Later&lt;/strong&gt; and a ✓ appears on it, so you know it's safe to close.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sleep.&lt;/strong&gt; Tabs you aren't using give their memory back but stay in your tab bar. Tabby reads your computer's free memory before and after, so the number it shows is measured, not estimated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close the copies.&lt;/strong&gt; Same page open four times? One click closes the extras, with Undo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⌘K (Ctrl+K)&lt;/strong&gt; finds any saved or open tab as you type.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coming from Toby?&lt;/strong&gt; No account needed, and your collections import in one click. &lt;strong&gt;Using OneTab?&lt;/strong&gt; Your list imports in one click too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free for good, not a trial:&lt;/strong&gt; saved tabs, collections and spaces (unlimited), Bring it back, Sleep, ⌘K and closing copies. Pro is optional (details below).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it can see:&lt;/strong&gt; your open tabs' titles and addresses, so it can save and restore them. It can't read what's on your pages, and nothing leaves your computer: no account, no analytics.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://chromewebstore.google.com/detail/tabby/olgjlfcacknhhlgcclkicjafbiaogjnc" rel="noopener noreferrer"&gt;Add to Chrome&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href="https://addons.mozilla.org/firefox/addon/tabby-tab-manager/" rel="noopener noreferrer"&gt;Add to Firefox&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the most tabs you've ever lost at once, and what did you do about it?&lt;/strong&gt; Tell me in the comments. (I have 34 open right now. Yes, I know.)&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Pro details, so there are no surprises: $4/month or $29/year. Five more themes, switching windows between more than 5 spaces, and Tidy up straight into collections. New users get a 14-day Pro trial, no card needed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>claude</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built Tabby, a privacy-first Chrome extension to tame tab chaos</title>
      <dc:creator>taewoo</dc:creator>
      <pubDate>Thu, 08 Oct 2026 12:30:34 +0000</pubDate>
      <link>https://dev.to/taewoo/i-built-tabby-a-privacy-first-chrome-extension-to-tame-tab-chaos-o9o</link>
      <guid>https://dev.to/taewoo/i-built-tabby-a-privacy-first-chrome-extension-to-tame-tab-chaos-o9o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure: this is a promotional post.&lt;/strong&gt; Tabby is my own extension. It's free, with an optional Pro tier coming later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;You know the feeling: 60+ tabs open, half from a bug you fixed last week, half from a project you'll "get back to". You don't close them because closing feels like losing them.&lt;/p&gt;

&lt;p&gt;I wanted a tool that makes closing tabs feel safe. So I built &lt;strong&gt;Tabby&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🗂 Spaces: switch projects, not windows
&lt;/h3&gt;

&lt;p&gt;Each space keeps its own open tabs. Switch spaces and your window swaps to that project's tabs, while the old ones are saved automatically. Pinned tabs (mail, chat) stay in every space, and tabs reopen unloaded, so even a 100-tab project switches instantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⌨️ Tidy up in a minute
&lt;/h3&gt;

&lt;p&gt;Go through your open tabs one at a time, oldest first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;K&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;X&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Close&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;S&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Save to Inbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Undo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Duplicates and links you've already saved are flagged while you decide.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 ⌘K / Ctrl+K search
&lt;/h3&gt;

&lt;p&gt;One search box for saved links, collections and open tabs. It stays fast with thousands of links.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✂️ Smart titles
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;fix: retry webhooks · Pull Request #4821 · GitHub&lt;/code&gt; becomes &lt;code&gt;fix: retry webhooks · #4821&lt;/code&gt;. Small thing, but your saved lists become readable.&lt;/p&gt;

&lt;h3&gt;
  
  
  🛟 Never lose a tab
&lt;/h3&gt;

&lt;p&gt;Automatic session backups, undo on every delete, a restorable Trash, and export to JSON or bookmarks anytime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy by design
&lt;/h2&gt;

&lt;p&gt;No account. No tracking. No ads. Your tabs and links are stored only on your device and never sent anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming from Toby or OneTab?
&lt;/h2&gt;

&lt;p&gt;Tabby imports Toby JSON exports, OneTab lists and browser bookmarks from Settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://chromewebstore.google.com/detail/tabby/olgjlfcacknhhlgcclkicjafbiaogjnc" rel="noopener noreferrer"&gt;Tabby on the Chrome Web Store&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's v1.0, my first published extension, so feedback, bug reports and feature ideas are very welcome in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>extensions</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Design patterns aren't a catalog — they're a way to predict where change will hurt</title>
      <dc:creator>taewoo</dc:creator>
      <pubDate>Thu, 10 Sep 2026 16:51:56 +0000</pubDate>
      <link>https://dev.to/taewoo/design-patterns-arent-a-catalog-theyre-a-way-to-predict-where-change-will-hurt-o0d</link>
      <guid>https://dev.to/taewoo/design-patterns-arent-a-catalog-theyre-a-way-to-predict-where-change-will-hurt-o0d</guid>
      <description>&lt;p&gt;Most design pattern content stops at "here's what a Factory is." That's fine for a quiz, but it doesn't help you when you're staring at a real codebase deciding how to structure the fifth integration you've onboarded this year.&lt;/p&gt;

&lt;p&gt;I spent a recent afternoon deliberately drilling on pattern recognition — not memorizing names, but learning to ask the right question before writing code. This post is the distilled version, applied to a problem a lot of backend engineers eventually hit: onboarding a new external partner whose rules, data formats, and failure modes don't quite match the ones you already built for.&lt;/p&gt;

&lt;p&gt;The specific partners here are placeholders. The shape of the problem is real, and you don't need a finance background to follow it — swap "PSP" for "payment provider," "shipping carrier," or "auth provider" and the same reasoning applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Imagine you're integrating with three outside partners — call them Partner A, Partner B, and Partner C. Each one has its own quirks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each requires some kind of upfront check before you're allowed to proceed — but the pass/fail rules differ.&lt;/li&gt;
&lt;li&gt;Each wants identity or ID data in a different format.&lt;/li&gt;
&lt;li&gt;Each exposes a completely different API — different endpoint names, different field names, and a different way of signing requests.&lt;/li&gt;
&lt;li&gt;Each needs different things to happen afterward: a log entry, a notification to your ops team, a cache update.
None of that is exotic — it's the everyday reality of "we support multiple vendors now." The naive approach is one big service class with a long case statement branching on partner name. It works fine at first. It stops working the moment you onboard Partner D and discover that adding one new check means touching a 150-line method that three other partners also depend on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The questions that actually matter
&lt;/h2&gt;

&lt;p&gt;Before reaching for a pattern name, here's the mental shift that made the real difference for me: a pattern isn't defined by how many steps there are, or whether there's a conditional. It's defined by &lt;strong&gt;what changes independently, and who owns the interface.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three questions I now ask myself before writing the code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Can this step stop the next one from running?&lt;/strong&gt; If yes, you're looking at a chain, not just a sequence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did I design this interface myself, or am I adapting to one that already exists?&lt;/strong&gt; If the shape came from someone else's API, you're translating (Adapter). If you invented the interface so several implementations can be swapped in, you're choosing behavior (Strategy).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When this step finishes, do several independent things react without needing to agree with each other — or is exactly one thing supposed to happen?&lt;/strong&gt; Independent, uncoordinated reactions is Observer. Picking exactly one behavior out of several is Strategy — even if, confusingly, the code that does the picking looks a lot like a Factory.
That third question is the one I got wrong the most, so it's worth sitting with for a second. A conditional that picks between objects doesn't tell you the pattern. What the object does next does.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With that in mind, here's how the four steps of the flow actually map out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping the flow, step by step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Steps 1 &amp;amp; 2 — the upfront checks → Chain of Responsibility
&lt;/h3&gt;

&lt;p&gt;Both checks can halt everything downstream. The first check fails → stop, skip the second check, never call the partner's API. The second check fails → stop, never call the API. That veto power over the rest of the flow is the defining trait of Chain of Responsibility: each step is independent, and any one of them can end the whole thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FirstCheck&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fail!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:first_check&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;passes?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecondCheck&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fail!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:second_check&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;passes?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;CHAIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;FirstCheck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SecondCheck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="no"&gt;CHAIN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;check&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;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;failed?&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's easy to confuse this with Template Method, which also runs a fixed sequence of steps. The difference is authority: Template Method is one class where a subclass fills in the steps, but no individual step gets to cancel the whole flow. If your steps can independently veto everything after them, that's the tell for Chain of Responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — calling the partner's API → Adapter, with Strategy hiding inside it
&lt;/h3&gt;

&lt;p&gt;Each partner's API is a shape they defined, not you. Different field names, different endpoints — that's the textbook case for Adapter: translate a shape you don't control into the one your own application wants to depend on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PartnerAAdapter&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# translate params to Partner A's field names, call their endpoint&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;em&gt;how&lt;/em&gt; you sign the request — one scheme vs. another — is a separate decision, sitting one layer inside the adapter. You designed that interface yourself, and more than one implementation can be swapped in at runtime. That's Strategy, even though it physically lives inside an Adapter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HmacSigner&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PartnerAAdapter&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;signer: &lt;/span&gt;&lt;span class="no"&gt;HmacSigner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@signer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signer&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson here: a single step in a flow can need two patterns stacked on top of each other. Don't force one pattern to cover a whole step just because it's "step 3" — ask the diagnostic question separately for the outer shape (talking to the partner) and the inner behavior (how you sign).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — what happens afterward → Observer
&lt;/h3&gt;

&lt;p&gt;Logging, notifying ops, updating a cache — three independent reactions to "the account was created," and none of them need to know the others exist, and none of them are chosen over the others.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccountCreatedBroadcaster&lt;/span&gt;
  &lt;span class="no"&gt;LISTENERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;AuditLogger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;OpsNotifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CacheUpdater&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;LISTENERS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the one most often confused with Strategy, and the confusion isn't really about "how many branches run" — it's about coordination. Strategy picks one behavior for a task. Observer runs all reactions to an event, with zero coordination between them. Counting branches is a symptom, not the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually cost me to learn
&lt;/h2&gt;

&lt;p&gt;I want to be honest about the parts I got wrong, because the mistakes taught me more than the correct answers did.&lt;/p&gt;

&lt;p&gt;I reached for Builder any time I saw "multiple steps" — even when the steps were really a validation chain that could halt midway, nothing like a builder object with &lt;code&gt;.set_x&lt;/code&gt; methods. I reached for Factory any time I saw a conditional picking an object, even when the object being picked was really doing Strategy's job and the conditional was incidental. I reached for Adapter for two interfaces I'd designed myself from scratch, when there was nothing to translate — that was Strategy the whole time.&lt;/p&gt;

&lt;p&gt;The fix wasn't memorizing more examples. It was swapping "what does this look like" for "what changes independently, and who owns the interface." That question works on code I haven't seen yet, which is really the whole point of learning a pattern in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;This is the first post in a series working through everyday infrastructure problems as they actually show up in backend systems — idempotency, race conditions, deadlocks, and reconciliation are next. Same approach every time: not "here's the definition," but "here's the messy case where the textbook answer didn't quite fit, and what I had to work out instead."&lt;/p&gt;

&lt;p&gt;If your codebase is turning into a wall of case statements every time you onboard something new, a pattern name isn't the fix by itself. The three questions above are what actually help you find the seam before you write the code.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://taewoo.me/posts/design-patterns-arent-a-catalog" rel="noopener noreferrer"&gt;taewoo.me&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Starting here</title>
      <dc:creator>taewoo</dc:creator>
      <pubDate>Tue, 25 Aug 2026 03:25:31 +0000</pubDate>
      <link>https://dev.to/taewoo/starting-here-17oo</link>
      <guid>https://dev.to/taewoo/starting-here-17oo</guid>
      <description>&lt;p&gt;I work on backend systems, mostly payment infrastructure — integrating with payment&lt;br&gt;
partners, handling things like a single HMAC signature or one reconciliation line&lt;br&gt;
that looks small but isn't. Get it wrong and it's not a typo, it's real money gone.&lt;/p&gt;

&lt;p&gt;A lot of that work happens alongside AI now. It writes code faster than I do. But it&lt;br&gt;
doesn't know how a specific PSP actually behaves in production until it breaks —&lt;br&gt;
usually at the worst time, while the next feature is already due. That part is&lt;br&gt;
still mine.&lt;/p&gt;

&lt;p&gt;This isn't a place where I play industry expert — I'm not, and that's not the point.&lt;br&gt;
It's where I write down what's actually true: what I understand, what I don't, and&lt;br&gt;
the parts I still check by hand no matter how confident the AI sounds.&lt;/p&gt;

&lt;p&gt;If you're working at that same edge — using AI to move faster while still being the&lt;br&gt;
one who answers for it when it breaks — some of this might be useful. No big&lt;br&gt;
promises. Just writing it down, regularly, and seeing where it goes.&lt;/p&gt;

</description>
      <category>payment</category>
      <category>ai</category>
      <category>meta</category>
    </item>
  </channel>
</rss>
