İçeriğe geç

Servis Sınırları — Bounded Context ve Database-per-Service

Orta 10 dk Çok sık karşılaşılır

Önce şunu oku: Monolit mi, mikroservis mi?

30 saniyede özet

Servisleri tablolara göre bölersen (müşteri, ürün, sipariş) her iş bütün servisleri dolaşır. Yeteneğe göre bölüp her servise kendi verisini verirsen, bir iş çoğunlukla tek serviste başlar ve biter.

Mikroservise geçerken ilk refleks, veritabanı tablolarına bakıp her birine bir servis vermektir: CustomerService, ProductService, OrderService. Düzenli görünür. İlk “sipariş ver” isteği gelene kadar.

  1. Bayt: Her tabloya bir servis verdim: müşteri, ürün, sipariş. Ne kadar düzenli!

  2. Sen: Peki biri sipariş verince ne oluyor?

  3. Bayt: Sipariş bütün servisleri tek tek dolaşıyor. Biri yavaşlasa hepsi bekliyor!

  4. Bayt: Muhasebe ile lojistik aynı müşteri dosyasını mı paylaşmalı, yoksa ikisinin de kendi dosyası mı olmalı?

Tablolara göre bölmek: her şeyi herkese sormak

Varlıklara göre bölünmüş bir sistemde sipariş vermek şöyle görünür:

OrderService.java (varlığa göre bölünmüş)
public OrderId place(PlaceOrderCommand cmd) {
Address address = customerClient.addressOf(cmd.customerId()); // ağ
Money price = productClient.priceOf(cmd.sku()); // ağ
productClient.decreaseStock(cmd.sku(), cmd.quantity()); // ağ
paymentClient.charge(cmd.customerId(), price); // ağ
return orders.save(new Order(…)).id();
}

Tek bir kullanıcı isteği dört senkron çağrı oldu. Gecikmeler toplanır ve dört servisten biri bile düşse sipariş verilemez.

Sorun servis sayısı değil, sınırın yeri. İş akışları isimleri keser; isimlere göre çizilen sınırlar da iş akışlarını keser.

Kafam karıştı, daha basit anlat

Tablolara göre bölersen, tek bir sipariş için dört servise sormak zorunda kalırsın. Biri bile cevap vermezse sipariş verilemez.

Hızlı kontrolBaşlangıç

Sistem CustomerService, ProductService, OrderService ve PaymentService olarak bölünmüş. "Sipariş ver" isteğinde ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aynı kelime, üç anlam

“Ürün” her yerde aynı şey değil:

Üç context, üç Product
// Catalog: müşteriye ne gösterilir
record Product(Sku sku, String title, List<Image> images, Money listPrice) {}
// Inventory: depoda ne var
record StockItem(Sku sku, WarehouseId warehouse, int onHand, int reserved) {}
// Billing: nasıl faturalanır
record BillableItem(Sku sku, TaxRate taxRate, Money unitPrice) {}

Bir modelin ve kelimelerinin tutarlı olduğu bu sınıra bounded contextBir modelin ve kelimelerinin tek, tutarlı bir anlam taşıdığı sınır. "Ürün" katalog context'inde başka, stok context'inde başka bir modeldir.Sözlükte gör → denir. İyi bir servis sınırı genellikle bir bounded context’tir: katalog, sipariş, stok, faturalama.

Her context kendi verisinin sahibidir. Buna database-per-serviceHer servisin kendi verisine sahip olması ve bu veriye diğer servislerin yalnızca onun API'si ya da olayları üzerinden ulaşması. Tablolar paylaşılmaz.Sözlükte gör → denir: başka bir servisin verisine yalnızca onun API’si ya da yayınladığı olaylar üzerinden ulaşılır, tablosuna asla doğrudan.

Kafam karıştı, daha basit anlat

“Ürün” kelimesi vitrinde bir şey, depoda başka, kargoda bambaşka bir şey anlatır. Her bölümün kendi “ürün”ü olması doğaldır.

Hızlı kontrolOrta

"Ürün" katalogda başlık, görsel ve fiyat; stokta depo ve adet; faturada vergi oranı demek. DDD bunu nasıl ele alır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Ordering servisi fiyatı öğrenmek için neden Catalog'un product tablosunu doğrudan okumamalı?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Satır satır: sipariş vermek bir sınırın içinde

Yeteneğe göre bölünmüş bir sistemde Ordering servisi fiyatın bir kopyasını kendi tutar. Catalog fiyatı değiştirdiğinde bir olay yayınlar, Ordering kopyasını günceller.

Catalog servisi tamamen çökmüşken bu sistemde sipariş verilebilir mi? Cevabı göster

Evet. Ordering fiyatı kendi kopyasından okur ve yalnızca stok için Inventory’ye senkron gider. Bedeli şu: Catalog’daki son fiyat değişikliği henüz gelmediyse sipariş birkaç saniye eski fiyatla verilir.

Tablolara göre kesmek ya da iş yeteneklerine göre kesmek.
Adım adım oku
  1. Servisler tablolara göre bölününce tek bir sipariş Customer, Product ve Inventory'yi sırayla çağırır.
  2. Zincirdeki bir servis yavaşlarsa sipariş vermek de yavaşlar; biri çökerse sipariş verilemez.
  3. Yeteneğe göre bölünen Ordering, işi için gereken fiyat ve adres bilgisinin kendi kopyasını tutar.
  4. Catalog fiyatı değiştirince bir olay yayınlar ve Ordering kendi kopyasını günceller; sipariş kendi sınırı içinde verilir.

İş akışı tek bir sınırın içinde

PlaceOrder.java
1@Service
2class PlaceOrder {
3 private final PriceBook prices; // Catalog fiyatlarının yerel kopyası
4 private final InventoryClient inventory;
5 private final OrderRepository orders;
6 private final Outbox outbox;
7
8 @Transactional
şu an çalışan satır public OrderId place(PlaceOrderCommand cmd) {
10 Money price = prices.priceOf(cmd.sku());
11 inventory.reserve(cmd.sku(), cmd.quantity());
12 Order order = orders.save(Order.place(cmd, price));
13 outbox.add(new OrderPlaced(order.id(), order.total()));
14 return order.id();
15 }
16}
17
18@KafkaListener(topics = "catalog.price-changed")
19void on(PriceChanged event) { prices.update(event.sku(), event.newPrice()); }

Debug

Adım 1/7

Ordering İstek Ordering'e geldi. Adres, sepet ve müşteri bilgisi komutun içinde.

Java 21UTF-8LF9:1

Sol/sağ ok tuşlarıyla da gezebilirsin.

Veriyi kopyalamak bağımsızlık kazandırır, anlık tutarlılığı satar. Kopyanın bir süre eski kalabilmesine eventual consistencySistemin bir süre tutarsız kalıp sonunda tutarlı hâle gelmesi. Saga'nın verdiği garanti budur; arada geçilen ara durumu tasarımın kabul etmesi gerekir.Sözlükte gör → denir. Bunun kabul edilebilir olup olmadığı teknik değil, bir iş kararıdır.

Hızlı kontrolOrta

Ordering, Catalog'un yayınladığı PriceChanged olaylarıyla fiyatların yerel bir kopyasını tutuyor. Bunun bedeli nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Yeteneğe göre bölünmüş sistemde Catalog servisi tamamen çökmüş. Sipariş verilebilir mi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

Aynı dükkân üç farklı biçimde bölünmüş. Dört senaryo sırayla çalışıyor: sipariş ver, fiyat değiştir, iade et, bir kolonu değiştir.

Servis sınırları — her senaryo ne ödüyor?

Tohum 859596
  • Customer
  • Product
  • Order
  • Payment

Her servisin kendi veritabanı var

Senaryo başına maliyet
SenaryoSenkronOlayYabancı tabloBirlikte deploy
Sipariş ver––––
Fiyat değiştir––––
İade et––––
product.price kolonunu price_amount + currency yap––––
Hız
Adım 0

Şu an ne oldu?

Varlığa göre (her isim bir servis)

Aynı dükkân, dört senaryo. Adımla ve her senaryonun servisler arasında ne kadar konuşma gerektirdiğini gör.

Aklında kalsın: Sınır doğru çizilmişse, bir senaryonun işi büyük ölçüde tek bir servisin içinde biter.

Görevler0/3

  • "Sipariş ver" için 4 senkron çağrı üretaçık

    İpucu

    Varsayılan stratejiyle ilk senaryoyu çalıştır. Her isim bir servisse, sipariş hepsine sormak zorunda.

  • Siparişi en fazla bir senkron çağrıyla ve başka servisin tablosuna dokunmadan veraçık

    İpucu

    Ordering fiyatın bir kopyasını kendi tutabilir. Hangi strateji "ürün"ün her context'te farklı bir şey olmasına izin veriyor?

  • Tek bir kolon değişikliğinin üç servisi birden kırdığını göraçık

    İpucu

    Paylaşılan veritabanı stratejisini seç ve dört senaryoyu da çalıştır.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

  1. Varlığa göre bölünmüş hâliyle çalıştır. “Sipariş ver” dört senkron çağrı tutuyor, iade üç.
  2. Yeteneğe göre bölünmüş hâle geç. Sipariş bir senkron çağrı ve bir olayla bitiyor. Fiyat değişikliği yalnızca bir olay.
  3. Paylaşılan veritabanını seç. Senkron çağrı sıfır — ilk bakışta en iyisi gibi. Ama servisler birbirinin tablosuna yazıyor ve son senaryoda tek bir kolon değişikliği üç servisi birden kırıyor.
  4. Üç görevi de tamamla.
Kafam karıştı, daha basit anlat

Simülatörde her senaryonun kaç servise dokunduğuna bak. İyi çizilmiş sınırda bir değişiklik çoğu zaman tek bir servisin içinde kalır.

Hızlı kontrolOrta

Simülatörde paylaşılan veritabanı stratejisinde hiç senkron çağrı yok. Neden yine de en kötü seçenek?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Tuzaklar

Paylaşılan veritabanı ağ çağrısı yok diye cazip görünür. Ama tablo şeması birden çok servisin ortak sözleşmesi olur ve bir değişiklik hepsini birlikte deploy etmeyi gerektirir. Servisler ayrı süreçlerde çalışır ama ayrı değişemez: bu bir dağıtık monolitServisleri ayırıp bağımsızlığı kazanamamış sistem: ortak veritabanı, koordineli deploy, senkron zincirler.En kötü bileşim — monolitin kolaylığını kaybeder, mikroservisin faydasını almazsın. Teşhis sorusu tek: bir servisi tek başına deploy edebiliyor musun?Sözlükte gör →.

Kopyanın sahibini karıştırmak: Ordering’deki fiyat bir kopyadır, doğrusu Catalog’dadır. Kopyayı Ordering içinde değiştirmek iki doğru kaynak yaratır. Kopyalar yalnızca olaylarla güncellenir.

Erken bölmek. Domain henüz iyi bilinmiyorsa sınırlar yanlış çizilir, ve yanlış bir servis sınırını taşımak pahalıdır. Modüler bir monolitle başla, sınırları önce kod içinde doğrula.

Takım yapısını yok saymak. Bir servisi iki takım değiştiriyorsa, ya da bir takım her özellik için beş servise dokunuyorsa, sınır takım yapısıyla çelişiyor demektir. İyi sınırlar genellikle takım sınırlarıyla örtüşür.

Aşağıdaki örnek bir bankadan: “müşteri” kimlik doğrulamada, kredide ve kartta başka şeyler demek. Kredi servisi kararını kendi sınırı içinde veriyor.

Derinleş · Bankada bounded context: kredi kendi sınırında 5 dosya · ~102 satır · ilk okumada atlayabilirsin
Proje dosyaları

src/main/java/com/bank/contexts/ ThreeCustomers.java Aynı kelime, üç model: KYC için müşteri kimlik ve belgedir, kredi için borçlu gelir ve risktir, kart için kart sahibi limit ve ekstredir.

src/main/java/com/bank/contexts/ThreeCustomers.java
// One listing for comparison; in the codebase each lives in its own service.
// KYC (onboarding): who is this person, and did we verify it?
record KycProfile(CustomerNo no, String nationalId, String fullName, List<Document> documents,
VerificationStatus status) {}
// Lending: can this person repay?
record Borrower(CustomerNo no, BigDecimal monthlyIncome, BigDecimal totalExposure,
RiskRating riskRating, boolean kycVerified) {}
// Cards: what may this person spend, and what do they owe?
record Cardholder(CustomerNo no, String embossedName, BigDecimal creditLimit, LocalDate statementDay) {}

src/main/java/com/bank/lending/ BorrowerRecord.java Kredi context'inin kendi borçlu modeli: KYC durumu ve risk notu, KYC servisinden gelen olaylarla tutulan yerel bir kopya.

src/main/java/com/bank/lending/BorrowerRecord.java
@Entity
@Table(name = "borrowers")
class BorrowerRecord {
@Id
private String customerNo;
private BigDecimal monthlyIncome; // owned by lending: declared in the application
private BigDecimal totalExposure; // owned by lending: sum of its own loans
// Copied from KYC events. Lending never writes these and never asks KYC synchronously.
private boolean kycVerified;
@Enumerated(EnumType.STRING)
private RiskRating riskRating;
private long kycVersion; // the last KYC event applied
void applyKyc(boolean verified, RiskRating rating, long version) {
if (version <= kycVersion) return; // an older or duplicate event: ignore
this.kycVerified = verified;
this.riskRating = rating;
this.kycVersion = version;
}
Borrower view() {
return new Borrower(new CustomerNo(customerNo), monthlyIncome, totalExposure, riskRating, kycVerified);
}
}

src/main/java/com/bank/lending/ KycEventsListener.java KYC olaylarını dinleyip kopyayı güncelleyen tüketici. Olay iki kez gelirse sürüm karşılaştırması eskiyi yok sayar.

src/main/java/com/bank/lending/KycEventsListener.java
@Component
class KycEventsListener {
private final BorrowerRepository borrowers;
KycEventsListener(BorrowerRepository borrowers) {
this.borrowers = borrowers;
}
@KafkaListener(topics = "kyc.customer-status", groupId = "lending")
@Transactional
void on(CustomerStatusChanged event) {
borrowers.findOrCreate(event.customerNo())
.applyKyc(event.verified(), event.riskRating(), event.version());
}
}

src/main/java/com/bank/lending/ ApproveLoan.java Kredi kararı ağ çağrısı yapmadan verilir; sonuç outbox ile yayınlanır. KYC servisi düşükken de başvuru değerlendirilir.

src/main/java/com/bank/lending/ApproveLoan.java
@Service
class ApproveLoan {
private final BorrowerRepository borrowers;
private final LoanRepository loans;
private final CreditPolicy policy;
private final Outbox outbox;
ApproveLoan(BorrowerRepository borrowers, LoanRepository loans, CreditPolicy policy, Outbox outbox) {
this.borrowers = borrowers;
this.loans = loans;
this.policy = policy;
this.outbox = outbox;
}
@Transactional
public Decision decide(LoanApplication application) {
Borrower borrower = borrowers.get(application.customerNo()).view(); // local, no network
if (!borrower.kycVerified()) return Decision.pending("KYC_NOT_VERIFIED");
Decision decision = policy.evaluate(borrower, application.amount(), application.termMonths());
if (decision.approved()) {
Loan loan = loans.save(Loan.approved(application, decision.rate()));
// Disbursement belongs to core banking: tell it, do not call it.
outbox.add(new LoanApproved(loan.id(), application.customerNo(), application.amount()));
}
return decision;
}
}

src/main/resources/db/migration/ V3__lending.sql Kredi servisinin kendi şeması: KYC tablolarına foreign key yok, yalnızca müşteri numarası.

src/main/resources/db/migration/V3__lending.sql
-- Lending's own database. No foreign key to KYC or cards: other contexts are
-- reached through their events and APIs, never through their tables.
CREATE TABLE borrowers (
customer_no VARCHAR(20) PRIMARY KEY,
monthly_income NUMERIC(15, 2),
total_exposure NUMERIC(15, 2) NOT NULL DEFAULT 0,
kyc_verified BOOLEAN NOT NULL DEFAULT FALSE,
risk_rating VARCHAR(10),
kyc_version BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE loans (
id BIGINT PRIMARY KEY,
customer_no VARCHAR(20) NOT NULL REFERENCES borrowers (customer_no),
principal NUMERIC(15, 2) NOT NULL,
annual_rate NUMERIC(6, 4) NOT NULL,
term_months INT NOT NULL
);

Kendini sına

Şimşek turu1/5

Servisleri veritabanı tablolarına göre bölmek, tek bir işi birçok servise dağıtabilir.

Soru 1/3İleri

Aşağıdakilerden hangisi dağıtık monolitin en güçlü belirtisidir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aklında kalacak üç şey

  1. 1 Tablolara göre bölünmüş servisler çok konuşur: tek bir sipariş, adres, fiyat, stok ve ödeme için dört ayrı çağrıya dönüşür. İyi bir sınırda iş çoğunlukla tek serviste biter.
  2. 2 Aynı kelime farklı yerlerde farklı anlama gelir. Katalog, stok ve fatura kendi 'ürün'ünü tutar; başkasının verisi gerekirse olaylarla bir kopyası alınır.
  3. 3 Ortak veritabanı ağ çağrısını kaldırır ama bağımlılığı tabloya taşır. Birlikte yayına çıkmak zorunda olan servisler bağımsız değildir.
Sonraki kapı Yavaşça ölen bir servis, onu çağıran servisi nasıl yanında götürür? Circuit Breaker · 10 dk

5 kart sonraki derste seni bekliyor

0/5 kart bu dersten toplandı

Bu dersin üstüne kurulanlar

Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.