<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Architecture on omegaatt</title>
    <link>https://www.omegaatt.com/tags/architecture/</link>
    <description>Recent content in Architecture on omegaatt</description>
    <generator>Hugo -- 0.164.0</generator>
    <language>zh-TW</language>
    <managingEditor>raiven.kao@gmail.com (Raiven Kao)</managingEditor>
    <webMaster>raiven.kao@gmail.com (Raiven Kao)</webMaster>
    <copyright>Raiven Kao 2020 - 2026</copyright>
    <lastBuildDate>Tue, 11 Aug 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://www.omegaatt.com/tags/architecture/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Spec-First API：用 codegen 約束 LLM 與人類的 drift</title>
      <link>https://www.omegaatt.com/blogs/develop/2026/spec_first_api_against_drift/</link>
      <pubDate>Tue, 11 Aug 2026 00:00:00 +0000</pubDate><author>raiven.kao@gmail.com (Raiven Kao)</author>
      <guid>https://www.omegaatt.com/blogs/develop/2026/spec_first_api_against_drift/</guid>
      <description>&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;
&lt;p&gt;事情是這樣開始的。&lt;/p&gt;
&lt;p&gt;我請 agent 幫忙加一個 endpoint，它三分鐘寫完，handler 乾淨、error handling 完整、unit test 也跟著補齊，&lt;code&gt;go test ./...&lt;/code&gt; 全綠。此時我想到的是，agent 真的有做完嗎？route 真的有被註冊嗎？&lt;/p&gt;
&lt;p&gt;換成三年前的我，也會漏，那要怎麼樣讓 Agent 不會漏？這個系統從一開始就沒有任何機制能發現有人漏改。以前它靠的是 reviewer 的眼力與同一批人的肌肉記憶撐著；現在把產出速度乘上十倍，這條防線就直接被沖垮了。這不是 agent 的問題，是架構債被提早引爆。&lt;/p&gt;
&lt;p&gt;這篇想記錄的，就是後來把這個服務從 code-first 遷移到 spec-first 的過程：用 OpenAPI 當單一真值來源(SSOT)，用 oapi-codegen 產生 typed interface，讓 compiler 與 CI 去做原本靠人記憶的事情。&lt;/p&gt;
&lt;h2 id=&#34;問題不是有三份檔案&#34;&gt;問題不是「有三份檔案」&lt;/h2&gt;
&lt;p&gt;遷移前，同一條 API 的定義散落在三個地方：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;infra route definition     ← path / method / authorizer
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cmd/*/main.go              ← mux.HandleFunc 手寫註冊
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Go swag comments           ← @Router / @Param / @Success
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;三份檔案本身不是問題，同一個概念散在多個檔案是常態。&lt;/p&gt;
&lt;p&gt;真正的問題是，它們彼此不知道對方存在。&lt;/p&gt;
&lt;p&gt;改 path，可能只改到 Go route，忘了 infra；改 response shape，可能只改 handler struct，忘了註解；授權設定只活在 infra 層，Go 跟 OpenAPI 都不知道有這回事。而 swaggo 的註解在 repo 裡形同空轉，它是「程式碼的產物」，而我們沒有任何機制拿它回頭驗證程式碼。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Pragmatic Clean Architecture in Go</title>
      <link>https://www.omegaatt.com/blogs/develop/2026/pragmatic_clean_architecture_in_go/</link>
      <pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><author>raiven.kao@gmail.com (Raiven Kao)</author>
      <guid>https://www.omegaatt.com/blogs/develop/2026/pragmatic_clean_architecture_in_go/</guid>
      <description>&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;
&lt;p&gt;在大型系統或需要長期維護的產品中，導入 DDD（Domain-Driven Design）或 Clean Architecture 幾乎是業界的標準答案，分層清晰、職責明確、可測試性高，這些優點毋庸置疑。&lt;/p&gt;
&lt;p&gt;但問題是，不是每個專案都是大型系統。&lt;/p&gt;
&lt;p&gt;當你面對的是一個內部工具、side project、或是剛起步的新產品，Clean Architecture 的大全套 Use Case、Port、Adapter、Aggregate、Repository Interface 全放 domain，往往會讓你在還沒寫第一行商業邏輯之前，就先在資料夾結構裡迷路了三個小時，或是讓不熟悉的貢獻者花費大量時間在閱讀架構。&lt;/p&gt;
&lt;p&gt;這篇文章想討論的是：在不犧牲可測試性與可維護性的前提下，哪些抽象可以捨棄、哪些值得保留，以及我自己踩過的一些坑。&lt;/p&gt;
&lt;h2 id=&#34;捨棄什麼&#34;&gt;捨棄什麼&lt;/h2&gt;
&lt;h3 id=&#34;獨立的-portadapter-層&#34;&gt;獨立的 Port/Adapter 層&lt;/h3&gt;
&lt;p&gt;Clean Architecture 中，Use Case 透過明確定義的 input/output port 與外界溝通，搭配 Adapter 負責格式轉換。這在大型系統中確實有其價值，但它帶來的代價是：為了讓每一層都能獨立替換，你需要維護大量的介面與轉換邏輯，而這些轉換邏輯往往只是把 A struct 的欄位複製到 B struct。&lt;/p&gt;
&lt;p&gt;在小專案中，這條轉換鏈可以大幅縮短。HTTP handler 本身就可以負責 DTO 的轉換，不需要再多一層 Adapter。&lt;/p&gt;
&lt;h3 id=&#34;ddd-的-aggregate&#34;&gt;DDD 的 Aggregate&lt;/h3&gt;
&lt;p&gt;Aggregate 是 DDD 中保護業務不變式（invariant）的邊界，透過 Aggregate Root 統一管理相關物件的存取。這個概念本身沒有問題，但在小專案中，如果業務規則還不夠複雜到需要嚴格的不變式保護，過早引入 Aggregate 反而會讓簡單的 CRUD 操作變得冗長。&lt;/p&gt;
&lt;h2 id=&#34;保留什麼&#34;&gt;保留什麼&lt;/h2&gt;
&lt;p&gt;捨棄了部分複雜度之後，剩下的四層架構已經足以應付絕大多數的小專案需求。&lt;/p&gt;
&lt;h3 id=&#34;package-domain&#34;&gt;&lt;code&gt;package domain&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;這裡只放 &lt;strong&gt;Domain Model&lt;/strong&gt; 與 &lt;strong&gt;Value Object&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;值得特別說明 Domain Model 的定義。它不等同於 DDD 裡的 Domain，不是什麼深奧的設計概念，Domain Model 只是在描述「這個應用程式如何跟外部世界互動、邊界在哪」，也就是你的核心資料結構與它們身上的行為。有些「賣課」的人喜歡把 Domain Model 直接等同於 DDD 的 Domain，實際上它是個更基礎、更普遍的概念，可以參考這支&lt;a href=&#34;https://www.youtube.com/watch?v=aSPu0WhVPew&#34;&gt;影片&lt;/a&gt;的解釋。Domain Model 中不需要也不該出現任何技術細節，例如回傳是否是 JSON 等等，這些 Domain Model 甚至要能讓 PM 與非技術人員也「聽的懂」。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Interface 不是有開就好：從一個 PR 來看抽象化的重要性</title>
      <link>https://www.omegaatt.com/blogs/develop/2025/interface-is-not-just-about-creating-one/</link>
      <pubDate>Sat, 04 Oct 2025 00:00:00 +0000</pubDate><author>raiven.kao@gmail.com (Raiven Kao)</author>
      <guid>https://www.omegaatt.com/blogs/develop/2025/interface-is-not-just-about-creating-one/</guid>
      <description>&lt;h2 id=&#34;前言&#34;&gt;前言&lt;/h2&gt;
&lt;p&gt;最近團隊正在開發一個新產品，其中一個核心功能需要 client 與 server 之間進行即時、雙向的溝通。經過一番技術評估，我們決定採用 WebSocket 來實現這個需求。&lt;/p&gt;
&lt;p&gt;身為一個良好習慣的開發團隊，我們在開發初期就導入了依賴注入（Dependency Injection），希望透過界面（Interface）來解耦商業邏輯與具體的實作，這樣不僅能提高程式碼的可測試性，未來在更換底層實作時也能更加輕鬆。&lt;/p&gt;
&lt;p&gt;一切聽起來都很美好，直到我在一次 Code Review 中，看到了一段熟悉的程式碼。&lt;/p&gt;
&lt;h2 id=&#34;一個-pr-的故事&#34;&gt;一個 PR 的故事&lt;/h2&gt;
&lt;p&gt;在我們的 Domain Layer，也就是處理核心商業邏輯的地方，我看到同事定義了下面這個 interface：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// package/to/domain/service.go&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// WebSocketService defines the interface for websocket communication.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WebSocketService&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// StartAndLinsten starts the service and listens for incoming messages.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;StartAndLinsten&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// Send sends a message to the client.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;Send&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;message&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;any&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;// ... other methods&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第一眼看過去，好像沒什麼大問題。有名稱、有方法、也確實是個 interface。然而，當我細看 &lt;code&gt;WebSocketService&lt;/code&gt; 這個命名時，總覺得哪裡怪怪的。&lt;/p&gt;
&lt;p&gt;於是我在 PR 上留下了這樣的 comment：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;這個界面主要是抽象化 client 與 server 間的互動，不應該侷限於 WebSocket 這個 Protocol。假如我們未來要換成使用 socket.io 或是 gRPC stream，是不是連 domain 層的 interface 也要跟著改動？&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
