ElementAPI mimarisi
ElementAPI Türkçe bir kimya atlasıdır: 118 element, 214 bileşik ve kısa öğrenme rotaları. Yanında sanal bir ticaret demosu çalışır (para birimi KREDI, gerçek para ve kargo yok). Bu sayfa projenin nasıl kurulduğunu anlatır: hangi servis ne yapar, servisler nasıl konuşur, veri nasıl tutarlı kalır.
Kaynak kod · Lisans MIT
Genel görünüm
Tarayıcı yalnızca web uygulamasıyla ve API kapısıyla (gateway) konuşur. Gateway istekleri servislere iletir. Servisler birbirine doğrudan çağrı yapmak yerine RabbitMQ üzerinden olay gönderir. Her servisin kendi veritabanı vardır; başka bir servisin tablosuna kimse dokunmaz.
Atlası tek başına açmak için ayrı bir science servisi de var (docker-compose.science.yml, :5080). Veritabanı ve broker kullanmaz.
Servisler
| Servis | Dil | Ne yapar | Korunan kural |
|---|---|---|---|
| gateway | .NET 10, YARP | Tek giriş noktası: yönlendirme, kimlik doğrulama, hız sınırı | Durum tutmaz |
| identity | .NET 10 | Hesap, oturum, API anahtarı, webhook aboneliği | Anahtar ve webhook sayısı sınırı, iptal edilen anahtar bir daha çalışmaz |
| catalog | .NET 10 | Element verisi, piyasa fiyatları | Fiyat her tamamlanan sipariş için bir kez hareket eder |
| compound | .NET 10 | Bileşik verisi (salt okuma) | Yok; yalnızca testler |
| order | Node 22, TypeScript | Siparişi alır, saga'yı yönetir | Sipariş yalnızca izin verilen geçişlerle ilerler |
| wallet | Java 21, Spring Boot | KREDI bakiyesi, varlıklar, defter | Bakiye eksiye düşmez; bir sipariş bir kez tahsil, bir kez iade edilir |
| inventory | Java 21, Spring Boot | Stok ve rezervasyon | Stok asla fazla satılmaz; rezervasyon bir kez kapanır |
| shipment | .NET 10 | Gönderim kaydı (simülasyon) | Bir sipariş için gönderim bir kez açılır |
| notification | .NET 10 | E-posta ve webhook bildirimleri | Her webhook X-Element-Delivery kimliği taşır, alıcı tekrarı atabilir |
Sipariş akışı (saga)
Bir sipariş beş servise dokunur ama hiçbiri ötekinin veritabanına yazmaz. Dağıtık bir transaction yerine order servisi adımları sırayla yönetir (orkestrasyon). Her adım, kendi servisinde ayrı bir yerel transaction'dır. Bir adım başarısız olursa order, önceki adımları geri alan telafi olaylarını gönderir.
Durumlar
Submitted ──StockReserved──▶ StockReserved ──PaymentProcessed──▶ Shipping ──ShipmentDispatched──▶ Completed
│ │ │
└─ StockReservationFailed └─ PaymentFailed └─ ShipmentFailed
veya zaman aşımı ─────────────── veya zaman aşımı ──────────────── veya zaman aşımı ──▶ Failed
Completed ve Failed son durumlardır, bir daha değişmez. Geçiş tablosu tek bir yerde durur (order-service/src/domain/sagaTransitions.ts); tabloya uymayan, geç ya da tekrar gelen olay sipariş üzerinde hiçbir şey değiştirmez.
Hangi olayı kim dinler
| Olay | Gönderen | Dinleyen ve yaptığı |
|---|---|---|
OrderSubmittedEvent | order | inventory: stok ayırır |
StockReservedEvent / StockReservationFailedEvent | inventory | order: ödemeye geçer / siparişi düşürür |
PaymentRequestedEvent | order | wallet: KREDI çeker |
PaymentProcessedEvent / PaymentFailedEvent | wallet | order: gönderime geçer / siparişi düşürür |
ShipmentRequestedEvent | order | shipment: gönderim kaydı açar |
ShipmentDispatchedEvent / ShipmentFailedEvent | shipment | order: tamamlar / siparişi düşürür |
AssetsCreditedEvent | order | wallet: alınan gramı kullanıcının varlıklarına ekler |
OrderCompletedEvent | order | inventory: ayrılan stoğu kesin düşer · catalog: fiyatı bir kez yukarı iter |
OrderStockReleaseEvent | order (telafi) | inventory: ayrılan stoğu geri bırakır |
PaymentRefundRequestedEvent | order (telafi) | wallet: çekilen KREDI'yi iade eder (çekilmediyse hiçbir şey yapmaz) |
UpdateOrderStatusEvent | order | notification: kullanıcıya e-posta ve webhook gönderir |
ElementSoldEvent | wallet | inventory: stoğa geri ekler · catalog: fiyatı bir kez aşağı iter |
Örnek senaryolar
Aşağıdaki senaryolar sistemin hem normal akışta hem de bir şeyler ters gittiğinde ne yaptığını adım adım gösterir.
1. Mutlu yol: 10 g altın siparişi
- Tarayıcı
POST /api/v1/ordersisteğiniIdempotency-Keybaşlığıyla gönderir. Gateway oturumu doğrular ve isteği order'a iletir. - order fiyatı ve görünen stoğu catalog'dan, bakiyeyi wallet'tan sorar. Stok yetmiyorsa hemen
409, bakiye yetmiyorsa402döner. Bu ağ çağrıları transaction'dan önce yapılır; transaction açıkken başka bir servisi beklemek kilitleri uzatırdı. - order tek transaction'da siparişi
Submittedolarak yazar, aynı transaction'da outbox tablosuna iki olay ekler:UpdateOrderStatusEventveOrderSubmittedEvent. Kullanıcı hemen202 Acceptedalır; gerisi arka planda yürür. - Outbox relay commit edilmiş satırları RabbitMQ'ya yayımlar ve broker onayını bekler.
- inventory,
Austok satırını kilitler (FOR UPDATE), 10 g ayırır, mesaj kimliğini inbox'a veStockReservedEvent'i outbox'a aynı transaction'da yazar. - order siparişi kilitler,
StockReserveddurumuna geçer vePaymentRequestedEventyazar. - wallet cüzdan satırını kilitler, bakiyeyi kontrol eder, KREDI'yi çeker, deftere kayıt atar,
PaymentProcessedEventyazar. - order
Shippingdurumuna geçer; shipment gönderim kaydını açıpTRK-…takip numarasıylaShipmentDispatchedEventgönderir. - order
Completedolur.AssetsCreditedEventile wallet 10 g altını varlıklara ekler;OrderCompletedEventile inventory ayrılan stoğu kesin düşer, catalog altının fiyatını bir kez yukarı iter. - Her durum değişikliğinde notification kullanıcıya bildirim gönderir.
2. Stok yetmiyor
order, istek anında stoğu zaten kontrol eder. Bu senaryo kontrolle rezervasyon arasında başka bir siparişin stoğu almasıyla oluşur. Ön kontrol yalnızca bir kolaylıktır; asıl kararı stok satırını kilitleyen inventory verir.
- inventory stok satırını kilitler ve
stok − ayrılmış < istenenolduğunu görür. - Hiçbir şey ayırmadan
StockReservationFailedEventgönderir. - order siparişi
Failedyapar ve telafi olaylarını yazar: stok bırakma ve iade. - İki telafi de bu durumda boşa çalışır: ayrılmış stok ve çekilmiş KREDI yoktur. Telafiler idempotent yazıldığı için "gerekmeyen telafi" zarar vermez. Order hangi adımın gerçekten yapıldığını takip etmek zorunda kalmaz.
3. Bakiye yetmiyor
Bu durum da ön kontrolden sonra bakiye değişirse oluşur (örneğin aynı anda verilen ikinci bir sipariş).
- Stok ayrıldı, order
PaymentRequestedEventgönderdi. - wallet cüzdanı kilitler; bakiye yetmediği için
Walletaggregate'i tahsilatı reddeder veINSUFFICIENT_ELXnedeniylePaymentFailedEventgönderir. - order
Failedolur;OrderStockReleaseEventile inventory ayrılan 10 g'ı geri bırakır. - Kullanıcı "Ödeme başarısız" bildirimini alır. Bakiyesi ve stok, sipariş hiç verilmemiş gibi kalır.
4. Kredi tavanını aşan sipariş
Toplam 50.000 KREDI'yi aşarsa order isteği HTTP katmanında 400 ile reddeder; saga hiç başlamaz. Aynı kural Order aggregate'inde ve wallet'ta da var. Biri atlansa bile öteki yakalar.
5. Aynı istek iki kez geldi (çift tıklama, ağ kopması)
- İstemci aynı
Idempotency-Keyile isteği tekrarlar. - order bu anahtarı sipariş kimliği olarak kullandığı için mevcut siparişi bulur ve onu
200ile döner. İkinci sipariş oluşmaz, KREDI iki kez çekilmez. - Aynı anahtar farklı bir sipariş gövdesiyle gelirse yanıt
409olur.
6. Aynı mesaj iki kez geldi
- wallet KREDI'yi çekti, commit etti, ama RabbitMQ'ya onay (ack) göndermeden çöktü.
- RabbitMQ
PaymentRequestedEvent'i yeniden teslim eder. - wallet yeni transaction'da mesaj kimliğini
processed_messagestablosuna yazmaya çalışır; kimlik zaten var, işlem orada durur. KREDI ikinci kez çekilmez. - catalog'da aynı koruma sipariş başına bir işaretle yapılır: fiyat bir sipariş için yalnızca bir kez hareket eder.
7. Broker kapalıyken sipariş verildi
- RabbitMQ yeniden başlıyor. order siparişi ve outbox satırlarını yine de commit eder; kullanıcı yanıtını alır.
- Relay yayımlayamaz, satırlar
pendingkalır. Hiçbir olay kaybolmaz. - Broker döndüğünde relay satırları sırayla yayımlar ve her birini broker onayından sonra işaretler.
- Outbox olmasaydı iki kötü sonuçtan biri olurdu: önce yayımla sonra commit et (commit başarısızsa var olmayan bir sipariş için olay gider) ya da önce commit et sonra yayımla (yayım başarısızsa sipariş sonsuza kadar
Submittedkalır).
8. Son 5 gram için iki eş zamanlı sipariş
- İki
OrderSubmittedEventaynı anda inventory'ye ulaşır. - İkisi de aynı stok satırını
FOR UPDATEile kilitlemeye çalışır; ikincisi birincinin commit'ini bekler. - Birinci 5 g'ı ayırır. İkinci, kilidi aldığında güncel değeri okur, stok kalmadığını görür ve
StockReservationFailedEventgönderir. - Kod bir gün yanlış yazılsa bile veritabanındaki
stock_items_never_oversoldCHECK kısıtı fazla satışı reddeder. Bu senaryo Testcontainers ile gerçek PostgreSQL üzerinde test ediliyor; kilit kaldırıldığında test kırılıyor.
9. Bir servis cevap vermiyor (zaman aşımı)
- shipment servisi kapalı; sipariş
Shippingdurumunda bekliyor. KREDI çekilmiş, stok ayrılmış. - order'daki süpürücü düzenli aralıklarla süresi geçmiş saga'ları arar. Varsayılan süreler:
Submitted120 sn,StockReserved120 sn,Shipping180 sn. - Süresi geçen siparişi kilitler, süreyi kilit altında yeniden kontrol eder (bu arada gerçek bir olay gelip siparişi ilerletmiş olabilir), sonra
Failedyapar ve stok bırakma + iade gönderir. - shipment geri gelip geç bir
ShipmentDispatchedEventgönderirse order bunu yok sayar;Failedson durumdur. Düşen bir siparişe geç gelen ödeme onayı ise yalnızca kendi iadesini tetikler.
10. Varlık satışı (masadan satış)
- Kullanıcı
POST /api/v1/desk/sellile 3 g altın satar. - wallet tek transaction'da varlığı azaltır, alış fiyatı (bid) üzerinden KREDI ekler, deftere yazar ve outbox'a
ElementSoldEventkoyar. - inventory 3 g'ı stoğa geri ekler; catalog altının fiyatını bir kez aşağı iter. İkisi de satışı bir kez işlemek için mesaj kimliğine bakar.
11. Sunucu yeniden başladı
- Docker bütün konteynerleri aynı anda başlatır; PostgreSQL birkaç saniye geç hazır olur.
- Servisler ilk bağlantı hatasını "geçici" olarak tanır ve 2 saniye arayla 30 kez dener. Yanlış şifre ya da bozuk migration gibi beklemekle düzelmeyecek hatalarda hemen durur, hata loglarda açıkça görünür.
- Bu sayede servisler restart döngüsüne girmez. Test: PostgreSQL 20 sn kapatıldığında order dışındaki servislerin restart sayısı 0 kaldı. order bilerek bir kez yeniden başlar; o sırada gelen mesajlar RabbitMQ'da bekler.
Tutarlılık kuralları (ACID)
Kurallar ADR 0003 içinde yazılı ve bütün servislerde uygulanıyor.
Bir iş, bir transaction
Her kullanım senaryosu tek bir veritabanı transaction'ıdır. Önce ilgili satır kilitlenir (SELECT … FOR UPDATE), karar ondan sonra verilir. Transaction içinde ağ çağrısı yapılmaz.
Transactional outbox
Giden olay, durum değişikliğiyle aynı transaction'da bir tabloya yazılır. Ayrı bir relay commit edilmiş satırları yayımlar ve broker onayını bekler. Veritabanı ile broker arasında "biri yazıldı, öteki yazılmadı" durumu oluşmaz.
Inbox
Olaylar en az bir kez gelir, yani tekrar gelebilir. Alıcı mesaj kimliğini işle aynı transaction'da kaydeder; aynı mesaj ikinci kez gelirse atlanır.
Veritabanı da korur
CHECK kısıtları ve tekil indeksler uygulama hatasına karşı son savunmadır. Kısıtlar NOT VALID eklenir, böylece eski bir satır açılışı bozup servisi restart döngüsüne sokmaz.
Para ondalık sayıdır
Para ve miktar her yerde BigDecimal, decimal veya NUMERIC olarak tutulur, double kullanılmaz.
Açılışta bekleme
Sunucu yeniden başladığında Docker bütün konteynerleri aynı anda açar. Servisler PostgreSQL hazır olana kadar bekler (30 deneme × 2 sn), çöküp yeniden başlamaz.
Servis içi katmanlar (DDD)
Korunacak bir kuralı olan servislerde kod üç katmana ayrılır; bağımlılıklar içe doğru akar.
| Katman | İçerik | Neye bağımlı |
|---|---|---|
domain | Aggregate, entity, değer nesneleri (Wallet, Money, StockItem, Order) | Hiçbir şeye: framework, SQL, broker yok |
application | Metot başına bir kullanım senaryosu; port arayüzleri (Repository, Outbox) | domain |
infrastructure, web, messaging | JDBC/EF adaptörleri, HTTP controller'lar, kuyruk dinleyicileri | application, domain |
Controller ve dinleyici girdiyi doğrular ve çevirir, repository saklar ve yükler; kararı aggregate verir. Kuralı olmayan servislere (compound, science, gateway) katman eklenmedi, yalnızca test yazıldı.
Testler
Domain kuralları önce test yazılarak geliştirildi; bu testler veritabanı kullanmaz ve milisaniyeler içinde biter. Transaction garantileri Testcontainers ile gerçek PostgreSQL üzerinde sınanır: eş zamanlı yazanlar kuralı bozamaz, durum ve outbox birlikte commit ya da rollback olur.
| Bölüm | Araç | Test sayısı |
|---|---|---|
| wallet | JUnit 5, Testcontainers | 28 |
| inventory | JUnit 5, Testcontainers | 28 |
| order | node:test | 38 |
| .NET servisleri | xUnit | 217 |
| web-app | Vitest | 148 |
| Uçtan uca duman testi | deploy/scripts/test-smoke.ps1 | 20 kontrol |
Çalıştırma
Docker Desktop açıkken:
Copy-Item docker/.env.example docker/.env # şifreleri kendin belirle ./deploy/scripts/present-platform.ps1 # http://localhost:6241 ./deploy/scripts/stop-local.ps1 # durdur
Yalnızca atlas için (veritabanı ve broker olmadan): ./deploy/scripts/present-local.ps1, adres http://127.0.0.1:5080.
Belgeler
- Servis kılavuzu: hangi servis ne işe yarar
- Sistem kılavuzu: her servisin uçları, mesajları ve kod haritası
- Mimari kararlar (ADR)
- Bilimsel katalog API sözleşmesi
- README