Catalog Service: Kendi Veritabanı, REST API ve Okuma Ağırlıklı Optimizasyonlar
Catalog service'i kendi veritabanının sahibi olan, okuma trafiğine göre ayarlanmış bir service'e dönüştürüyoruz: AsNoTracking projection ile arama ve sayfalama, Redis'te paylaşılan output cache ve tag ile eviction, URL segment versioning, ProblemDetails ve oasdiff ile denetlenen OpenAPI contract'ı. Fiyat ve isim değişiklikleri versiyonlu BookPriceChanged ve BookRenamed event'lerine dönüşüyor; aynı versiyon concurrency token olarak 8 paralel isteğin 4'ünü 409 ile durduruyor.
Sürüm notu: Bu yazıdaki kodlar .NET 10, Aspire 13.6, Asp.Versioning 10.2, Microsoft.AspNetCore.OpenApi 10.0 ve oasdiff 1.37 ile derlenip denenmiştir; output cache için Redis 8.6 kullanılmıştır.
Önce Tahmin Et
Catalog service'inin back office endpoint'iyle The Hobbit'in fiyatını değiştiriyorsun. Aynı anda sekiz kişi, sekiz farklı fiyatla aynı endpoint'i çağırıyor: 401, 402, ... 408 TL. Kitabın satırında bir Version kolonu var ve Entity Framework Core (EF Core) onu concurrency token olarak kullanıyor. Sekiz isteğin kaçı 204 döner, kaçı 409? Kitabın son fiyatı hangisi olur ve versiyonu kaça çıkar? Bir de: fiyat değiştikten hemen sonra kitap detayını isteyen müşteri, 10 dakikalık output cache'ten eski fiyatı mı görür? Cevaplar yazının sonlarına doğru, gerçek çıktılarla.
Monolith'ten Ne Taşıdık, Ne Bıraktık?
02'de Catalog'un veri modelini, sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) monolith'inden olduğu gibi kendi catalogdb'sine taşımıştık. Bu yazıda Catalog'u gerçek bir service yapıyoruz: kendi HTTP contract'ı, okuma ağırlıklı trafiğe göre ayarlanmış endpoint'leri ve dış dünyaya duyurduğu değişiklikleri olan bir service.
Monolith'te Catalog'un iki yüzü vardı. Biri Angular'a giden HTTP endpoint'leri, diğeri diğer module'lerin çağırdığı ICatalogService interface'i: FindByIsbnAsync, GetByIdsAsync. İkinci yüz service'e taşınmıyor. Ordering kitap bilgisine artık method çağrısıyla değil, 06'da Catalog'un event'leriyle beslediği kendi kopyasıyla ulaşacak. Bir şey daha bıraktık: monolith'teki kitap detay cevabında stok sayısı (Available) vardı ve Catalog onu Inventory module'ünden okuyordu. Stok Inventory'nin verisi; Catalog'un cevabından çıktı. Kitap sayfasında fiyatla stoğu yan yana göstermek 09'da BFF'in (Backend for Frontend; Angular'a özel ara backend) aggregation endpoint'inin işi olacak.
Bu iki karar Microservices serisinin Service Boundary yazısındaki entity service anti-pattern'ine karşı alındı. Catalog "kitap tablosunun HTTP önyüzü" olursa her service her istekte ona gider ve Catalog yavaşladığında bütün sistem yavaşlar. Catalog'un işi katalog: arama, listeleme, fiyat ve isim yönetimi.
Okuma Endpoint'leri: Arama ve Sayfalama
Catalog'un trafiğinin büyük kısmı okuma. Mimariye Giriş serisinde Bookstore için kampanya dönemlerinde katalog aramasına saniyede 200 istek hedefi koymuştuk. Arama endpoint'i monolith'teki hâlinden çok farklı değil; değişen şey dönen tip:
// GET /api/v1/books?search=austen&category=classics&page=1&pageSize=12
private static async Task<Ok<PagedResponse<BookSummaryResponse>>> SearchBooks(
CatalogDbContext db, string? search, string? category, int page = 1, int pageSize = 12, CancellationToken ct = default)
{
page = Math.Max(page, 1);
pageSize = Math.Clamp(pageSize, 1, 50);
IQueryable<Book> query = db.Books.AsNoTracking();
if (!string.IsNullOrWhiteSpace(search))
{
string term = search.Trim();
query = query.Where(b => b.Title.Contains(term) || b.Author.Name.Contains(term) || b.Isbn == term);
}
if (!string.IsNullOrWhiteSpace(category))
{
query = query.Where(b => b.Category.Slug == category);
}
int total = await query.CountAsync(ct);
List<BookSummaryResponse> items = await query
.OrderBy(b => b.Title)
.ThenBy(b => b.Isbn)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(b => new BookSummaryResponse(
b.Isbn, b.Title, b.Author.Name, b.Category.Name, b.Price, Data.Catalog.Currency, b.PublishedYear))
.ToListAsync(ct);
return TypedResults.Ok(new PagedResponse<BookSummaryResponse>(items, page, pageSize, total));
}
Üç ayrıntı var. AsNoTracking ve Select projection'ı entity'leri hiç oluşturmadan doğrudan cevap tipine okuyor. pageSize 50 ile sınırlı; sınırsız bir sayfa boyutu, Catalog'u tek bir istekle 40.000 satır okumaya zorlamanın en kısa yolu. ThenBy(b => b.Isbn) ise aynı başlıklı iki kitap olduğunda sıralamayı belirli hâle getiriyor; olmazsa offset pagination aynı kitabı iki sayfada gösterebilir. Sayfalama seçeneklerini ASP.NET Core Web API serisinin Pagination yazısında karşılaştırmıştık.
Dönen tipler service'in public contract'ı. Entity'ler service'ten hiç çıkmıyor:
// Catalog'un public HTTP contract'ı: entity'ler service'ten çıkmaz, bu record'lar çıkar
public sealed record BookSummaryResponse(
string Isbn, string Title, string Author, string Category, decimal Price, string Currency, int PublishedYear);
public sealed record BookResponse(
string Isbn, string Title, string Author, string Category, decimal Price, string Currency,
int PublishedYear, string Description, int Version);
public sealed record CategoryResponse(string Name, string Slug);
public sealed record PagedResponse<T>(IReadOnlyList<T> Items, int Page, int PageSize, int TotalCount);
Kitabın kimliği ISBN (International Standard Book Number; kitabın uluslararası kimlik numarası); Id kolonu contract'ta yok. Para birimi her cevapta açıkça yazıyor, çünkü 01'de konuştuğumuz ikinci ülke hedefi gerçekleştiğinde "fiyat hangi para biriminde?" sorusunu consumer'ın tahmin etmesini istemiyoruz. Çalışan service'ten bir arama:
curl -i "http://localhost:5101/api/v1/books?search=austen"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 14 Nov 2026 13:03:07 GMT
Server: Kestrel
Transfer-Encoding: chunked
api-supported-versions: 1.0
{"items":[{"isbn":"9780141439587","title":"Emma","author":"Jane Austen","category":"Classics","price":249.90,"currency":"TRY","publishedYear":1815},{"isbn":"9780141439518","title":"Pride and Prejudice","author":"Jane Austen","category":"Classics","price":239.90,"currency":"TRY","publishedYear":1813}],"page":1,"pageSize":12,"totalCount":2}
Title kolonuna bir index ekledik, ama Contains SQL'de LIKE '%austen%''e dönüşüyor ve baştaki joker karakter yüzünden bu index'i kullanamıyor. 20 kitapta fark yok, 40.000 kitapta var. Index, başlığa göre sıralamaya ve ileride "başlığı şununla başlayan" aramasına yarıyor. Gerçek bir katalog araması için doğru araç ayrı bir arama motoru; ilerideki Elasticsearch ile Arama serisinin konusu.
Output Cache: Redis'te Paylaşılan Cache
Katalog verisi saniyede bir değişmiyor ama saniyede yüzlerce kez okunuyor. Bu tablo output cache için ideal. ASP.NET Core output cache'in policy'lerini, vary-by kurallarını ve tag ile eviction'ı Caching serisinin Output Cache yazısında ayrıntılı anlattık; Catalog'da kurduğumuz şu:
public static void Configure(OutputCacheOptions options)
{
options.AddPolicy(BookLists, policy => policy
.SetVaryByQuery("search", "category", "page", "pageSize")
.Expire(TimeSpan.FromSeconds(60))
.Tag(BookListsTag));
options.AddPolicy(Book, policy => policy
.AddPolicy<BookTagPolicy>()
.Expire(TimeSpan.FromMinutes(10)));
options.AddPolicy(Categories, policy => policy.Expire(TimeSpan.FromMinutes(30)));
}
Liste sayfaları sorgu parametrelerine göre ayrı ayrı cache'leniyor ve hepsi book-lists tag'ini taşıyor. Kitap detayı 10 dakika cache'leniyor ve BookTagPolicy, route'taki ISBN'den book:9780441172719 gibi kitaba özel bir tag ekliyor; böylece tek bir kitabı cache'ten silebiliyoruz. Kategoriler neredeyse hiç değişmediği için 30 dakika.
Cache'in nerede durduğu microservice'te önemli. Varsayılan output cache store'u process'in belleğinde; Catalog'u üç instance'la çalıştırdığında her instance'ın ayrı bir cache'i olur ve bir instance'ta yaptığın eviction diğer ikisini temizlemez. Bu yüzden store'u Redis'e aldık. Aspire'ın Redis output cache integration'ı bunu tek satıra indiriyor, AppHost'ta da Catalog'a Redis referansı ekledik:
builder.AddSqlServerDbContext<CatalogDbContext>("catalogdb");
builder.AddRedisOutputCache("redis");
builder.Services.AddOutputCache(CachePolicies.Configure);
builder.AddProject<Projects.Bookstore_Catalog_Api>("catalog")
.WithReference(catalogDb)
.WithReference(redis)
.WaitForCompletion(catalogMigrator)
.WaitFor(redis)
.WithHttpHealthCheck("/health");
Aynı kitabı üç kez istedik:
GET book #1 -> 200 in 0.025201s
GET book #2 -> 200 in 0.002695s Age: 0
GET book #3 -> 200 in 0.001572s Age: 0
İlk istek veritabanına gidip cevabı Redis'e yazdı (25 ms), sonraki ikisi Redis'ten geldi (2-3 ms). Age header'ı cevabın cache'ten geldiğini gösteriyor. Notifications da Redis kullanacak, ama ikisi aynı Redis instance'ında farklı amaçlarla duruyor; bu bir paylaşılan veritabanı değil, çünkü hiçbir service diğerinin key'lerini okumuyor. Production'da ikisini ayırmak bir yük ve izolasyon kararı.
API Versioning ve ProblemDetails
Catalog'un contract'ı en az iki consumer'a hizmet edecek: BFF ve 04'te kitap kopyasını ilk kez doldururken Ordering. Bir alanı kaldırmak ya da anlamını değiştirmek gerektiğinde eski consumer'ları kırmadan yeni bir sürüm açabilmek için URL'ye baştan bir versiyon koyduk. Asp.Versioning'in seçeneklerini ASP.NET Core Web API serisinin Versioning yazısında anlattık; Catalog'un ayarı şu:
builder.Services.AddProblemDetails();
builder.Services.AddValidation();
builder.Services.AddApiVersioning(options =>
{
options.ApiVersionReader = new UrlSegmentApiVersionReader();
options.ReportApiVersions = true;
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
})
.AddOpenApi(options => options.Document.AddDocumentTransformer((document, _, _) =>
{
document.Info.Title = "Bookstore Catalog API";
document.Info.Description = "Books, authors, categories and prices. Owned by the Catalog & Search team.";
return Task.CompletedTask;
}));
RouteGroupBuilder v1 = app.NewVersionedApi("Catalog")
.MapGroup("/api/v{version:apiVersion}")
.HasApiVersion(1.0);
v1.MapGet("/books", SearchBooks).CacheOutput(CachePolicies.BookLists);
v1.MapGet("/books/{isbn}", GetBook).CacheOutput(CachePolicies.Book).ProducesProblem(StatusCodes.Status404NotFound);
v1.MapGet("/categories", GetCategories).CacheOutput(CachePolicies.Categories);
ApiVersionReader satırını ilk denemede yazmamıştık. Asp.Versioning 10.2 bir analyzer ile geliyor ve TreatWarningsAsErrors açık olduğu için build kırıldı: error AV0015: Configure 'ApiVersioningOptions.ApiVersionReader = new UrlSegmentApiVersionReader();' to optimize performance. Varsayılan reader versiyonu query string'de ve header'da da arıyor; yalnız URL segment kullandığımızı söyleyince her istekte bu arama yapılmıyor. ReportApiVersions ise her cevaba api-supported-versions: 1.0 header'ını ekliyor.
Hata cevaplarının hepsi ProblemDetails. Bilinmeyen bir ISBN:
curl -i http://localhost:5101/api/v1/books/9780000000000
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
Date: Sat, 14 Nov 2026 13:03:15 GMT
Server: Kestrel
Transfer-Encoding: chunked
api-supported-versions: 1.0
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Book not found","status":404,"detail":"There is no book with ISBN 9780000000000.","traceId":"00-01eda5c7efc6c77d0437795e90321c39-15073beccdc18349-03"}
traceId, 12. yazıda bir isteği service'ler arasında izlerken kullanacağımız W3C (World Wide Web Consortium; web standartlarını yayınlayan kuruluş) trace context formatındaki trace id; destek ekibine bu değeri söyleyen bir kullanıcı, log'larda tam olarak o isteği buldurur. Var olmayan bir sürümü (/api/v2/books) istediğimizde ise cevap 404 Not Found: URL segment versioning'de v2 route'u hiç yok. Fiyatı sıfır göndermek .NET 10'un yerleşik validation'ına takılıyor:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"Price":["The field Price must be between 0.01 and 100000."]},"traceId":"00-b95282465bef92b0d510b7bad6c23b40-9428216f60777ae6-03"}
Contract'ı Yayınlamak: OpenAPI Dokümanı ve Breaking Change Kontrolü
Catalog'un contract'ı consumer'lara nasıl ulaşıyor? Çalışan service /openapi/v1.json'da dokümanı sunuyor, ama bir consumer'ın contract'ı görmek için Catalog'u çalıştırması gerekmemeli. Microsoft.Extensions.ApiDescription.Server paketi, dokümanı her build'de dosyaya yazıyor; dosyayı repo'ya commit'liyoruz:
<PropertyGroup>
<RootNamespace>Bookstore.Catalog</RootNamespace>
<!-- OpenAPI dokümanı her build'de üretilir ve commit'lenir: consumer'ların okuduğu contract bu -->
<OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)/../openapi</OpenApiDocumentsDirectory>
<OpenApiGenerateDocumentsOptions>--file-name catalog</OpenApiGenerateDocumentsOptions>
</PropertyGroup>
Build sonunda src/Services/Catalog/openapi/catalog.json güncelleniyor. Contract değişirse pull request'te bu dosyanın diff'i görünüyor; code review'da "Catalog'un contract'ı değişti" bilgisi gözden kaçmıyor. Doküman build sırasında GetDocument.Insider adlı bir aracın içinde, uygulamanın host'u kurularak üretiliyor. İlk denemede başlık GetDocument.Insider | v1, açıklama da o aracın kendi açıklaması çıktı. Document transformer'daki Info.Title ve Info.Description atamaları bu yüzden var.
Dosyanın asıl değeri otomatik kontrolde. Bir developer'ın BookSummaryResponse'taki Title alanını Name olarak yeniden adlandırdığını düşün; C# tarafında her şey derleniyor. CI'da main'deki dokümanla yenisini oasdiff ile karşılaştırdık:
oasdiff breaking base.json catalog.json --fail-on ERR
1 changes: 1 error, 0 warning, 0 info
error [response-required-property-removed] at catalog.json
in API GET /api/v1/books
removed the required property `items/items/title` from the response with the `200` status
exit=1
Yeni alan (name) eklenmesi bir hata sayılmadı; geriye uyumlu bir değişiklik. Ama title'ın kaybolması, onu okuyan her consumer'ı kırar. --fail-on ERR olmadan oasdiff aynı raporu yazıp 0 ile çıkıyor; pipeline'ı durdurmak için bu bayrak şart. Kırıcı bir değişiklik gerçekten gerekiyorsa yol belli: v2'yi v1'in yanında açmak, consumer'ları taşımak, sonra v1'i kapatmak. Microservices serisinin Service Contract'ları yazısında anlattığımız expand-contract'ın HTTP'deki karşılığı bu. 13. yazıda aynı contract'ı consumer tarafından da Pact ile test edeceğiz.
Değişiklikleri Event'e Çevirmek: BookPriceChanged ve BookRenamed
Ordering, kitapların kendi kopyasını tutacak (01'deki karar). Kopyanın güncel kalması için Catalog'un "bu kitabın fiyatı değişti" ve "bu kitabın adı değişti" demesi gerekiyor. Integration event contract'ları Bookstore.Contracts'ta:
namespace Bookstore.Contracts.Catalog;
// Catalog'un yayınladığı integration event'ler. Version aynı kitaptaki her değişiklikte bir artar;
// consumer elindeki kopyadan eski bir event'i bu sayıyla tanıyıp yok sayabilir.
public sealed record BookPriceChanged(
string Isbn, decimal OldPrice, decimal NewPrice, string Currency, int Version, DateTimeOffset OccurredAtUtc);
public sealed record BookRenamed(
string Isbn, string OldTitle, string NewTitle, int Version, DateTimeOffset OccurredAtUtc);
Event'ler kendi içinde anlamlı: eski ve yeni değer, para birimi, zaman ve versiyon. Consumer'ın event'i işlemek için Catalog'u geri çağırması gerekmiyor; Messaging ve Event-Driven Architecture serisinde event-carried state transfer dediğimiz yaklaşım bu. Version alanı 06'da işe yarayacak: RabbitMQ aynı kitap için iki event'i ters sırada teslim ederse Ordering, elindeki versiyondan küçük olanı yok sayacak.
Event'i üreten yer Book. Monolith'te fiyat bir property'ydi ve herkes ona yazabiliyordu; şimdi değişiklik bir method'dan geçiyor ve method hem versiyonu artırıyor hem event'i kaydediyor:
// Concurrency token ve event versiyonu tek alanda: her değişiklik onu bir artırır
public int Version { get; private set; } = 1;
public void ChangePrice(decimal newPrice, TimeProvider clock)
{
if (newPrice == Price)
{
return;
}
decimal oldPrice = Price;
Price = newPrice;
Version++;
_events.Add(new BookPriceChanged(Isbn, oldPrice, newPrice, Catalog.Currency, Version, clock.GetUtcNow()));
}
CatalogDbContext'te Version bir concurrency token: book.Property(b => b.Version).IsConcurrencyToken().HasDefaultValue(1). Var olan satırlar için migration kolonu 1 ile dolduruyor. EF Core'un ürettiği UPDATE versiyonu WHERE'e ekliyor:
UPDATE [Books] SET [Price] = @p0, [Version] = @p1
OUTPUT 1
WHERE [Id] = @p2 AND [Version] = @p3;
Bu, Önce Tahmin Et'in cevabını veriyor. Sekiz paralel istek aynı satırı neredeyse aynı anda okudu. Aynı versiyonu okuyup yazmaya çalışanlardan yalnız ilki kazanabildi; diğerlerinin UPDATE'i 0 satır güncelledi ve EF Core DbUpdateConcurrencyException fırlattı. Gerçek çalıştırmanın sonucu:
price 402 -> 204
price 401 -> 409
price 407 -> 409
price 405 -> 204
price 406 -> 409
price 404 -> 204
price 408 -> 409
price 403 -> 204
Ardından kitabı okuduğumuzda fiyat 403, versiyon 5'ti. Dört istek başarılı oldu, çünkü istekler tam aynı anda değil, milisaniyeler arayla okuyup yazdı: bazıları bir önceki başarılı yazmadan sonra okudu ve yeni versiyonla devam etti. 409 alan dört istek ise "kitap sen okuduktan sonra değişti" ProblemDetails'i aldı. Son yazan kazanmadı: kaybeden istekler bir fiyatı sessizce ezmek yerine hata aldı. Concurrency token'ın davranışını Entity Framework Core serisinin Concurrency yazısında anlattık; burada önemli olan, aynı sayının event'in versiyonu olması. Ordering'e giden her BookPriceChanged'in versiyonu, Catalog'daki satırın o değişiklikten sonraki versiyonu.
Değişikliği işleyen ortak method, cache'i de temizliyor:
change(book);
try
{
await db.SaveChangesAsync(ct);
}
catch (DbUpdateConcurrencyException)
{
return TypedResults.Problem(
title: "Book was changed by another request",
detail: $"Book {isbn} was updated in the meantime. Read it again and retry.",
statusCode: StatusCodes.Status409Conflict);
}
// Hem detay sayfası hem bu kitabı içerebilecek bütün liste sayfaları artık eski
await cache.EvictByTagAsync(CachePolicies.BookTag(isbn), ct);
await cache.EvictByTagAsync(CachePolicies.BookListsTag, ct);
// 06'da bunlar transactional outbox'tan gidecek. O zamana kadar yalnız log'lanıyor:
// SaveChanges ile doğrudan publish arasında bir çökme event'i kaybettirir (dual write)
foreach (object integrationEvent in book.Events)
{
logger.LogInformation("Integration event ready: {EventType} {@Event}", integrationEvent.GetType().Name, integrationEvent);
}
book.ClearEvents();
return TypedResults.NoContent();
Önce Tahmin Et'teki ikinci sorunun cevabı burada: eviction yüzünden müşteri eski fiyatı görmüyor. Dune'un fiyatını 489'dan 459'a çektikten sonra detay isteği Age header'ı olmadan, veritabanından geldi ve "price":459.00,...,"version":2 döndü. Catalog'un log'unda da yayınlanmaya hazır event:
[catalog] Integration event ready: BookPriceChanged BookPriceChanged { Isbn = 9780441172719, OldPrice = 489.00, NewPrice = 459.00, Currency = TRY, Version
= 2, OccurredAtUtc = 11/14/2026 1:03:21 PM +00:00 }
Event'i şimdilik yalnız log'luyoruz ve bunu bilerek yapıyoruz. SaveChangesAsync'ten sonra RabbitMQ'ya doğrudan publish etseydik, iki işlem arasında process çökerse fiyat değişmiş ama event gitmemiş olurdu; Ordering'in kopyası bir daha düzelmezdi. Bunun adı dual write problemi ve çözümü transactional outbox: event, fiyat değişikliğiyle aynı transaction'da veritabanına yazılır, ayrı bir süreç onu broker'a taşır. Kavramı Messaging ve Event-Driven Architecture serisinin Transactional Outbox yazısında konuştuk; 06'da Wolverine'in EF Core outbox'ıyla Catalog'a ve Ordering'e kuracağız.
Üretimde Dikkat
Cache süresi bir business kararı. Kitap detayında 10 dakika güvenli, çünkü değişiklikte eviction var. Liste sayfalarında da eviction var, ama 60 saniyelik süre Redis'e ulaşılamadığı bir anda eviction kaçarsa bile eski fiyatın en fazla bir dakika görüneceğini garanti ediyor. Fiyatın siparişteki değeri cache'ten değil, Ordering'in kopyasından ve 04'te göreceğimiz sipariş anında dondurma kuralından geliyor; cache'teki birkaç saniyelik eskilik müşteriye yanlış fiyattan satış yapmak anlamına gelmiyor.
OpenAPI endpoint'i production'da dışarıya açık değil. Catalog BFF'in arkasında duruyor ve BFF yalnız kendi route'larını yayınlıyor; doküman build'de üretildiği için çalışan service'te sunulmasına ihtiyacımız yok. İstersen MapOpenApi()'yi Development ortamıyla sınırlayabilirsin.
Breaking change kontrolünü her service'in pipeline'ına koy, ama raporu da oku. oasdiff bir alanın kaldırılmasını yakalıyor; alanın anlamının değişmesini (fiyatın vergili hâlden vergisiz hâle geçmesi gibi) yakalayamaz. O tür değişiklikler contract test'lerinin ve code review'un işi.
Sık Düşülen Tuzaklar
Entity'yi döndürmek. Book'u doğrudan JSON'a çevirmek, veritabanı şemasını contract yapar; bir kolon eklemek bütün consumer'lara yeni bir alan gönderir, bir navigation property döngüsel JSON üretir. Response record'ları ayrı tutmak bu bağı koparıyor.
Başka service'in Catalog veritabanına bağlanması. "Raporlama için sadece okuyacağız" diye verilen bir connection string, Catalog'un şemasını değiştirmesini imkânsız hâle getirir. Rapor gerekiyorsa event'lerden beslenen ayrı bir read model ya da Catalog'un yayınladığı bir export, Microservices serisinin Data Ownership yazısındaki seçenekler.
Instance'a özel cache. In-memory output cache ile tek instance'ta her şey doğru görünür; ikinci instance açıldığı gün eviction yarım çalışır ve "bazen eski fiyat görünüyor" diye bir hata kaydı açılır.
Event'i entity değişmeden önce üretmek. Event'i endpoint'te elle oluşturmak, kaydetmenin başarısız olduğu durumda olmamış bir değişikliği duyurmaya ya da versiyonu yanlış yazmaya açık. Event'i durumu değiştiren method üretiyor, yayınlanması ise ancak kayıt başarılıysa oluyor.
Küçük Bir Deneme
★☆☆ /api/v1/categories'i iki kez iste ve Age header'ına bak. Kategoriler için eviction yok; bir kategorinin adını değiştiren bir endpoint ekleseydin hangi tag'i kullanırdın?
★★☆ GET /api/v1/books/{isbn} cevabına ETag header'ı ekle (değer Version'dan gelsin) ve fiyat değiştirme endpoint'ini If-Match header'ı gelmediğinde 428, eşleşmediğinde 412 dönecek şekilde değiştir. Önce Tahmin Et'teki sekiz paralel isteği tekrar çalıştır; sonuç nasıl değişiyor?
★★★ BookSummaryResponse'a Subtitle adında nullable bir alan ekle, build al ve oasdiff breaking'i çalıştır. Sonra aynı alanı required bir request alanı olarak RenameBookRequest'e ekle ve tekrar çalıştır. Hangisi kırıcı sayılıyor, neden?
Catalog kendi verisinin sahibi, okuma trafiğine hazır ve değişiklikleri event olarak tarif ediyor. Bir sonraki yazıda Ordering'i kuruyoruz: Domain-Driven Design serisinde zenginleştirdiğimiz Order aggregate'i kendi veritabanına taşıyacak, sipariş oluşturma command'ını sipariş listesinin projection'ından ayıracak, kitapların yerel kopyasını Catalog'un bu yazıdaki contract'ıyla ilk kez dolduracak ve fiyatı sipariş anında donduracağız.