iT邦幫忙

2025 iThome 鐵人賽

DAY 2
0

前言

在學習 微服務 (Microservices) 時,會遇到非常多的 terms 與 開發套件,而且有些概念很容易混淆。

  • SOA (Service-Oriented Architecture)
    是一個設計哲學,把大系統拆成多個可以互相溝通的小服務,透過共同的protocols(像 SOAP、HTTP)合作,來一起完成一個任務。
    就像一座城市裡的建築物,各自有功能,才能讓整個城市運作。

  • Microservice
    像是每棟建築物都有獨立的進出入口,服務小、獨立、通常是 HTTP-based。

  • Hexagonal Architecture
    像是每棟房子內的房間設計允許家具隨時更換,而不影響房子整體結構(保持乾淨邊界)-> it is about the internal。

What is Microservice

“An approach to distributed systems that promotes the use of finely grained services that can be changed, deployed, and released independently.” — Sam Newman, Building Microservices

微服務 (Microservice) 是一種處理 分散式架構 (Distributed System) 的方法。將一個大系統(大問題)拆解成小系統(每個小型服務就是一個小問題),這樣每個小服務可以,獨立開發、部署、測試。

The Trade-offs

每個設計思想都有正反兩面,微服務也帶來挑戰:

  • 架構更複雜:除了 code maintainence,還要解決"服務間"的溝通問題 (timeout、load balancing)。
  • 分散式特性: 網路延遲 (latency)、服務中斷 (denial)、資料一致性 (synchronization)。

微服務 並不總是最好的選擇 (not always good)。在小型專案或團隊人力不足的情況下,microservice 的複雜度可能會比它帶來的好處還多。

接下來我會簡述幾個設計理念

core 1 : Information Hiding

“Changes inside a microservice boundary shouldn’t affect an upstream consumer, enabling independent releasability of functionality.” — Sam Newman, Building Microservices

微服務應該隱藏內部細節 (implementation details),僅提供必要的 API 給外部使用。換句話說,微服務就是一個 Black Box。

core 2 : Self-Managed / Independent Deployability

每個微服務應該能獨立維護 (self-managed)。

  • 黑盒子內部的實作的改動,不應該影響其他服務。
  • 定義清楚 Microservice Boundary 能幫助我們達到 Self-Managed,通常我們會用 interface 來定義範圍。

舉例:
在本專案中,Java Spring Boot 是core app,他是一個訊息處理的平台。

  • 1 它如何發 HTTP request 去取得新聞作者、URL等資料。
  • 2 FastAPI 爬蟲服務 如何根據 URL 抓文章內容
    是兩件彼此獨立的事。這意味著,爬蟲可以自己運行,不需要因為我在 Spring Boot 裡修改了我如何發http request的套件使用就得重新修改或是部署爬蟲。

core 3 : Owning Their Own State

Don’t share databases unless you really need to… sharing databases is one of the worst things you can do if you’re trying to achieve independent deployability. — Sam Newman, Building Microservices

微服務架構中的一個重要原則是:盡量不要共享資料庫。

  • 每個微服務應該要 loose coupling ,可改成微服務各自有自己 DB。
  • 如果多個服務硬要共用一個 DB,會破壞獨立性,影響到測試及部署。

舉例:
這個專案有個資料庫,用來儲存從 API 抓到的新聞 URL。接著,爬蟲服務會根據這些 URL 抓文章。那抓下來的文章該怎麼放?爬蟲要不要直接存取資料庫?

最後我的解法是讓 Java Spring Boot 成為唯一能操作 DB 的入口。爬蟲只專心處理 URL → 文章的抓取,不自己維護 DB。

  • 好處:
    保持資料一致性 (consistency),避免 race condition。
    避免 schema 不一致。
  • 缺點:
    增加分散式的風險:如果某個服務掛掉或是 timeout,整體流程可能中斷。

Hexagonal Architecture (Ports & Adapters)

“Separate core business logic from entry points (customers) and exit points (suppliers, tools).” — Alistair Cockburn

  • Core Business Logic應該與 輸入端 (Entry Points) 和 輸出端 (Exit Points) 分離。
  • 系統內部應該保持乾淨且清楚的邏輯邊界,外部框架、資料庫、API 都只是 Adapters 不會影響到 core logic。

https://ithelp.ithome.com.tw/upload/images/20250916/20178775jGJYxhw0WT.png

https://ithelp.ithome.com.tw/upload/images/20250916/20178775xo9rCO0Csh.png

Domain-Driven Design (DDD)

DDD (Domain-Driven Design) 是一種幫助我們將 程式碼圍繞在 Business Logic (domain, core logic) 的設計哲學和方法。

  • 讓系統結構圍繞「domain」而不是技術。
  • 用 Entity、Value Object、Aggregate 來描述 core logic。
    • Entity / Value Object:資料定義 (DTOs)。
    • Use Case (Application Service):定義對domain可以有甚麼操作。
    • Domain Service:core bussiness logic。
    • Repository / Adapter:實作的細節(api client 實作, data type mapping等等)。

實作

用 DDD + Hexagonal 來定義 Microservice Boundary,使服務達到 Information Hiding。

  • Hexagonal Ports & Adapters:

    • In Port = AggregateFeedUseCase(外部可以怎麼用這個domain)
    • Out Port = ContentSourcePort(新聞來源的api client)
  • DDD 設計:

    • Entity = FeedItemSourceType
    • Service = AggregateFeedService
    • Repository / Adapter = 各個 API Client(Guardian、HackerNews,後續文章會提到)

Directory Layout :

├─domain
│  ├─model
│  │ FeedItem.java
│  │ SourceType.java 
│  ├─ports 
│  │  ├─in 
│  │  │ AggregateFeedUseCase.java
│  │  ├─out 
│  │  │ ContentSourcePort.java
│  ├─service → 
│  │ AggregateFeedService.java
  • model/ 主要放DTOs 物件資料型別來源種類
  • ports/ 定義可以跟新聞整合器互動的「插槽」。
    • ports/in/ 外部可以叫這個系統做什麼?
    • ports/out/ 這個系統需要新聞來源提供什麼?
  • service/ 核心邏輯,負責實際整合不同來源的新聞。

所以這裡就是在用 DDD :整個設計都圍繞著「如何處理商業邏輯」。而這domain是,我提供一個平台,讓使用者可以一問就得到新聞資料。至於實務上「每個新聞 API client的細節」要怎麼實作暫且不重要,就像黑盒子一樣我們會確保使用者可以拿到文章內容就好。(API client 實作細節會留到接下來的文章再介紹。)

Logic Diagram :

[ Application layer ]
        ↓ calls
[ IN port (use case) ]
  └─ AggregateFeedUseCase
        ↓ implemented by
[ Domain layer ]
  └─ AggregateFeedService (domain service)
        ↓ depends on
[ OUT ports ]
  └─ ContentSourcePort  
        ↑ implemented by
     news api client service (could be many!!)

FeedItem.java

public record FeedItem(
        String id,
        SourceType source,
        String title,
        String url,
        String author,
        Integer score,
        java.time.Instant publishedAt
) {}

SourceType.java

public enum SourceType {
    HN, GUARDIAN,
}

AggregateFeedUseCase.java

public interface AggregateFeedUseCase {
    // find the top <limitPerSource> number articles
    List<FeedItem> topAcrossSources(int limitPerSource);
}

ContentSourcePort.java

public interface ContentSourcePort {
    String sourceName();
    List<FeedItem> top(int limit);
    Optional<FeedItem> byId(String id);
}

AggregateFeedService.java

public class AggregateFeedService implements AggregateFeedUseCase {
    private final List<ContentSourcePort> sources;

    public AggregateFeedService(List<ContentSourcePort> sources) {
        this.sources = sources;
    }


    @Override
    public List<FeedItem> topAcrossSources(int limitPerSource) {
        return sources.stream()
                .flatMap(s -> s.top(limitPerSource).stream())
                .sorted(Comparator.comparing(FeedItem::publishedAt).reversed())
                .toList();
    }
}

Reference

  • Sam Newman, Building Microservices: Designing Fine-Grained Systems (2nd Edition)
  • Davi Vieira, Designing Hexagonal Architecture with Java (2nd Edition)
  • Alistair Cockburn, Hexagonal Architecture (2005)

上一篇
Day 1|冒險起點:系列介紹與開發環境準備
下一篇
Day 3|HTTP 三隻貓:RestTemplate、WebClient、OpenFeign
系列文
Spring 冒險日記:六邊形架構、DDD 與微服務整合6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言