Node.js'te Monolitten Mikroservislere Geçiş Rehberi
Node.js monolitlerini event-driven serverless fonksiyonlara ayrıştırma rehberi: migrasyon stratejileri, sınır kararları ve mimari kalıplar.
Monolitlerin Sürdürülemezliği#
Node.js monolitleri öngörülebilir biçimde bozulur. Tek bir ortak deployment sınırı, yavaş bir domain’in tüm release döngüsünü bloke etmesine izin verir; yoğun trafikte tek bir feature’da çıkan arıza aynı process’teki diğer her şeyi de birlikte götürür. Mimari çözüm monoliti yeniden düzenlemek değil; onu bağımsız olarak deploy edilebilen, event-driven fonksiyonlara ayrıştırmaktır.
Sonucu sınırlar belirler. Sınırları, kodun gerçekte nasıl değiştiğine ve nerede birlikte bozulduğuna bakarak seçin; bölme sürerken sistemi release edilebilir tutun. Böylece migrasyon her adımda geri alınabilir kalır.
Monolit Nasıl Büyüdü#
Bu şekildeki bir e-ticaret platformu genellikle sade bir Node.js Express uygulaması olarak başlar:
// İlk gün: tek process, tek deployment
app.use('/api/users', userController);
app.use('/api/products', productController);
app.use('/api/orders', orderController);
app.use('/api/inventory', inventoryController);
app.use('/api/payments', paymentController);
// ... ve onlarca controller daha
Zamanla bu uygulama karmaşık bir sisteme dönüşür:
- Büyük kod tabanı binlerce dosyaya yayılmış
- Birden fazla business domain tek bir repository’de
- Uzun deploy süreleri kapsamlı test suite’leri dahil
- Azalan takım hızı karmaşıklık arttıkça
- Yüksek altyapı maliyetleri monolitik deployment için
- Önemli debugging yükü geliştirme zamanını tüketiyor
Domain Sınırları Arasında Bilişsel Yük#
Monolit tartışmaları çoğu zaman teknik borçta düğümlenir. Asıl kısıt daha dardır: tek bir değişikliği çıkarmak için bir mühendisin kafasında tutması gereken sistem miktarı. Tipik bir “basit” feature aynı anda dört domain’e uzanır:
// Tek bir "ürün önerisi" endpoint'i dört domain'e dokunur:
// 1. User service (authentication + preferences)
class UserService {
async getUserPreferences(userId: string) {
// tercih çözümleme, onay bayrakları, segment sorgusu
// + tek bir metodun arkasında birden çok tablo
// + external profil entegrasyonları
}
}
// 2. Product service (catalog + inventory + pricing)
class ProductService {
async getRecommendations(userId: string, context: string) {
// tek imzanın arkasında birkaç sıralama stratejisi
// + ML model entegrasyonu
// + A/B test framework'ü
// + Cache invalidation logic (en zor kısım)
}
}
// 3. Order service (satın alma geçmişi analizi için)
class OrderService {
async getUserOrderHistory(userId: string, limit?: number) {
// geçmiş siparişler üzerinde çok tablolu join'ler
// + Veri gizliliği uyum logic'i
// + Yüksek hacimli hesaplar için ayarlanmış sorgu yolları
}
}
// 4. Analytics service (önerileri takip etmek için)
class AnalyticsService {
async trackRecommendationEvent(event: RecommendationEvent) {
// event zenginleştirme ve batch'leme
// + GDPR uyumu
// + Rate limiting
// + Queue management
}
}
Bu sınıfların hiçbiri tek başına mantıksız değil. Maliyet, tek bir endpoint dördüne birden bağlandığında ortaya çıkıyor: değişiklik küçük, ama o değişikliği güvenle yapmak için okunması gereken kod küçük değil. Onboarding de aynı nedenle yavaşlar; önce öğrenilecek daha küçük bir birim yoktur.
Feature Geliştirme Darboğazları#
Tek bir istek maliyeti somutlaştırır: alışveriş sepetinde ilgili ürünleri göstermek. Bir öğleden sonralık gibi görünen iş, bir diziye dönüşür:
- Mevcut mimariyi anlama birden fazla birbirine bağlı service boyunca
- Dikkatli entegrasyon mevcut iş akışlarını bozmamak için
- Kapsamlı test karmaşık test suite’lerinde regresyonu önlemek için
- İteratif düzeltmeler değişiklikler sistemin beklenmedik kısımlarını etkilediğinde
- Cascade debugging bir düzeltme başka bir bileşeni bozduğunda
- Basitleştirilmiş uzlaşma tam implementasyon çok riskli olduğunda
Her adımın kendisi teknik olarak zor değil. Zorluk, değişikliğin deployment biriminin zorlamadığı bir sınırı geçmesinden doğuyor; bu yüzden hiçbir parça izole doğrulanamıyor.
Yeniden Yazma Yerine Evrimsel Ayrıştırma#
Masaya genelde üç seçenek gelir: baştan yazmak, ekibi ciddi biçimde büyütmek veya artımlı ayrıştırma. Baştan yazmak, monolitin zaman içinde soğurduğu tüm hata düzeltmelerini çöpe atar ve sürdüğü boyunca feature geliştirmeyi dondurur. Ekibi büyütmek koordinasyon maliyetini ortadan kaldırmaz, artırır. Savunulmaya değer varsayılan artımlı ayrıştırmadır; çünkü her adım tek başına canlıya çıkar ve tek başına geri alınabilir.
Acı Odaklı Service Extraction#
Domain modelleri sınır belirlemek için zayıf bir ilk kaynaktır: işi anlatırlar. Deployment geçmişi ise doğrudan deployment’ı anlatır. İlk adayları seçmek için bu geçmişteki üç sinyal yeterlidir:
- Birlikte değişen bileşenler sıkı coupling’e işaret eder ve birlikte kalmalıdır
- Birlikte bozulan bileşenler yalnızca bir kod yolunu değil, bir risk sınırını paylaşır
- Farklı ölçekleme veya release ihtiyacı olan bileşenler extraction adayıdır
Birkaç aylık pull request ve incident kaydını bu sinyallere göre okumak kısa ve sıkıcı bir liste çıkarır:
| Grup | Üyeler | Sinyal |
|---|---|---|
| Sıkı bağlı | user-auth, notification, profile | neredeyse her zaman aynı release’te çıkar |
| Sıkı bağlı | product-catalog, inventory, pricing | tek bir şema değişikliği üçünü birden etkiler |
| Sıkı bağlı | order-processing, payment, shipping | birindeki hata diğerlerini geri aldırır |
| Extraction adayı | analytics | yazma ağırlıklı, farklı bir eğride ölçeklenir |
| Extraction adayı | admin-tools | ayrı release döngüsü, ayrı kullanıcı kitlesi |
| Extraction adayı | recommendations | hataları zaten izole ve tolere edilebilir |
Faz 1: Düşük Riskli Extraction’lar#
Hatanın ucuz olduğu yerden başlayın. İlk adaylar üç ortak özelliğe sahiptir:
- Zaten izole minimal paylaşılan bağımlılıklar ile
- Yüksek acı, düşük risk analytics ve admin araçları gibi
- Farklı operasyonel karakteristik ML öneri motorları gibi
İlk fazın kazandırdıkları:
- Hızlı deployment’lar çıkarılan servisler için, çekirdek release trenine dokunmadan
- İzole analytics, artık istek trafiğiyle yarışmıyor
- Bağımsız admin geliştirme, kendi iş akışı ve release döngüsüyle
- Prova imkânı: sonraki fazların dayandığı deployment, monitoring ve on-call değişiklikleri için
Faz 2: Temel Business Logic#
İkinci faz gelir-kritik bileşenleri ele alır: ürün yönetimi, sipariş işleme ve ödeme işleme.
Event-Driven İletişim:
Servisten servise HTTP, bölmenin ortadan kaldırması gereken coupling’i olduğu gibi korur. Amazon EventBridge, senkron zinciri downstream servislerin abone olduğu yayınlanmış olaylarla değiştirir:
import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';
const events = new EventBridgeClient({});
// Önce: Synchronous coupling
async function processOrder(orderData) {
// Synchronous bağımlılıklar cascade failure riski yaratır
const user = await userService.validateUser(orderData.userId);
const inventory = await inventoryService.reserveItems(orderData.items);
const payment = await paymentService.processPayment(orderData.payment);
const shipping = await shippingService.calculateShipping(orderData.address);
// Tek bir transaction'da birden fazla başarısızlık noktası
return await orderService.createOrder({ user, inventory, payment, shipping });
}
// Sonra: Event-driven dayanıklılık
async function processOrder(orderData) {
// Sipariş kaydını hemen oluştur
const order = await orderService.createOrder(orderData);
// Olguyu yayınla; downstream servisler bu olaya abone olur
const published = await events.send(new PutEventsCommand({
Entries: [{
Source: 'order-service',
DetailType: 'Order Created',
Detail: JSON.stringify({
orderId: order.id,
userId: orderData.userId,
items: orderData.items
})
}]
}));
// Reddedilen bir entry de başarıyla döner: hata exception olarak değil,
// yanıtın gövdesinde gelir. Kontrol edilmezse sipariş kaydedilir ve
// hiçbir abonenin haberi olmaz.
if (published.FailedEntryCount) {
const [entry] = published.Entries ?? [];
throw new Error(
`Sipariş ${order.id} publish edilemedi: ${entry?.ErrorCode} ${entry?.ErrorMessage}`
);
}
// Azaltılmış coupling ve cascade failure riski
return order;
}
Bu kontrol savunma amaçlı bir fazlalık değil. PutEvents, throttling dahil entry başına reddedilmeleri exception fırlatarak değil yanıtın içinde bildirir; kontrol edilmeyen bir çağrı, hiçbir abonenin görmediği bir siparişi kalıcı hale getirir. FailedEntryCount okumak, veritabanı yazması ile publish arasındaki boşluğu görünür kılar. Boşluğu kapatmak için outbox gerekir: event’i siparişle aynı transaction içinde yazın, ayrı bir publisher tabloyu boşaltıp yeniden denesin.
Faz 3: Serverless Fonksiyonlar#
Servis sınırları oturduğunda deployment birimi bir kez daha küçülebilir: servislerden fonksiyonlara:
Fonksiyon-Tabanlı Mimari:
Her servis, odaklanmış ve tek amaçlı fonksiyonlardan oluşan bir koleksiyona dönüşür:
// product-service/lib/clients.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { EventBridgeClient } from '@aws-sdk/client-eventbridge';
export const docClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const events = new EventBridgeClient({});
// product-service/functions/get-product.ts
import type { APIGatewayProxyEvent } from 'aws-lambda';
import { GetCommand } from '@aws-sdk/lib-dynamodb';
import { docClient } from '../lib/clients';
export const handler = async (event: APIGatewayProxyEvent) => {
const productId = event.pathParameters?.productId;
// Tek sorumluluk: Ürün verisini al
const product = await docClient.send(new GetCommand({
TableName: 'Products',
Key: { id: productId }
}));
return {
statusCode: 200,
body: JSON.stringify(product.Item)
};
};
// product-service/functions/inventory-updated.ts
import type { EventBridgeEvent } from 'aws-lambda';
import { UpdateCommand } from '@aws-sdk/lib-dynamodb';
import { PutEventsCommand } from '@aws-sdk/client-eventbridge';
import { docClient, events } from '../lib/clients';
type InventoryDetail = { productId: string; newQuantity: number };
export const handler = async (event: EventBridgeEvent<'Inventory Updated', InventoryDetail>) => {
const { productId, newQuantity } = event.detail;
// Tek sorumluluk: Envanter değişikliklerine tepki ver
await docClient.send(new UpdateCommand({
TableName: 'Products',
Key: { id: productId },
UpdateExpression: 'SET #inv = :qty',
ExpressionAttributeNames: { '#inv': 'inventory' },
ExpressionAttributeValues: { ':qty': newQuantity }
}));
// Gerekirse downstream event publish et
if (newQuantity === 0) {
const published = await events.send(new PutEventsCommand({
Entries: [{
Source: 'product-service',
DetailType: 'Product Out of Stock',
Detail: JSON.stringify({ productId })
}]
}));
// Yukarıdaki kuralın aynısı: sesli hata ver ki invocation yeniden denensin
if (published.FailedEntryCount) {
throw new Error(`Stok tükendi eventi düştü: ${productId}`);
}
}
};
Ayrıştırma Neyi Değiştirir#
Migrasyon yazıları birbirinden çok farklı iki tür kazanımı aynı torbaya koyar. İşe başlamadan önce bu ikisini ayırmak gerekir.
Bölmenin garanti ettiği: deploy kapsamı. Bir domain’deki değişiklik artık ilgisiz test suite’lerini beklemez; o domain kendi hızında release alır. Etki alanı da birlikte küçülür, çünkü çevresel bir servis kapalıyken checkout hizmet vermeye devam edebilir.
Ayrıca kazanılan: onboarding süresi, debugging hızı ve incident sayısı. Bunlar ancak her servis kendi verisine sahip olduğunda ve iletişim eventler üzerinden kurulduğunda iyileşir. Hâlâ aynı veritabanını paylaşan ve birbirini senkron çağıran servisler bu üçünü monolitteki halinden daha kötü yapar; çünkü coupling artık ağın üzerindedir.
Maliyet Profili#
| Boyut | Sürekli açık instance’larda monolit | Event-driven serverless |
|---|---|---|
| Compute faturası | Zirveye göre sağlanır, boşta da ödenir | Çağrı başına, sıfıra kadar iner |
| Monitoring | Kurulup işletilecek özel stack | CloudWatch dahili, X-Ray tracing bir bayrakla |
| Deployment | Ortak build ve deploy altyapısı | Fonksiyon başına deploy, yamalanacak host yok |
| Ölçekleme | Dikey, önceden planlanır | Yatay ve otomatik, hesap kotalarıyla sınırlı |
| Hata biçimi | Uygulamanın tamamı bozulur | Tek bir handler bozulur |
Fatura modeli düşmez, biçim değiştirir. Ani yükselen, event biçimli iş yükleri (analytics, bildirim, admin araçları) genellikle ucuzlar. İstikrarlı yüksek hacimli trafik ise Lambda’da istek başına doymuş bir instance’tan pahalıya gelebilir; yani tek başına compute tasarrufu migrasyon için zayıf bir gerekçedir.
Migrasyondan Sağ Çıkan Tasarım Kuralları#
Event-Driven İletişim Dayanıklılığı Artırır#
Asenkron iletişim izolasyon kazandırır. Kapalı olan bir tüketici, üreticiyi hataya düşürmek yerine işi geciktirir; yeter ki event’in bekleyeceği kalıcı bir yer olsun: bir dead-letter queue ve bir retry politikası, bunlar olmadan event basitçe kaybolur.
Fonksiyonlar Çoğu Business Logic Kalıbına Uyar#
Birçok iş yükünde tam bir servisin operasyonel yüzeyi ihtiyacı aşar. Odaklanmış bir fonksiyonun kontratı dardır, hata biçimi dardır ve stack trace’i tek ekrana sığar.
Observability Baştan Kurulur#
Logic dağıtıldıktan sonra “bu istek zamanını nerede harcadı” sorusunun tek yanıtı tracing’dir:
// Dağıtık fonksiyonlar için temel observability
import { captureAWSv3Client } from 'aws-xray-sdk-core';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
// Her SDK client'ını sar; çağrıları trace subsegment'ine dönüşsün
const dynamo = captureAWSv3Client(new DynamoDBClient({}));
export const handler = async (event, context) => {
// `dynamo` üzerinden yapılan çağrılar çağıranın trace'i ile korele olur,
// yeter ki fonksiyonda active tracing açık olsun.
};
Ayrıştırmanın Yanlış Gittiği Yerler#
Dağıtık Monolit#
Servisler kod düzenine göre bölünür, ama aynı veritabanını ve aynı senkron çağrı zincirini paylaşmaya devam eder. Baştaki coupling’lerin hepsi yerinde kalır; üstüne ağ timeout’ları, kısmi hatalar ve retry’lar eklenir: bir servis deploy edilirken diğeri geri alınamıyorsa, bölme yalnızca kozmetiktir.
Önce Çekirdeği Çıkarmak#
Checkout ve ödeme, herkesin monolitten ilk çıkarmak istediği bileşenlerdir ve yeni deployment modelini öğrenmek için en kötü yerdir. Pipeline, dashboard’lar ve on-call rotasyonu sıkıcı hale gelene kadar çevreyi çıkarın; çekirdeğe ondan sonra yaklaşın.
Hata Yolu Olmayan Eventler#
Başarılı bir publish, tüketici hakkında hiçbir şey söylemez. Her kural için bir dead-letter queue, bir retry politikası ve kuyruk derinliğine bir alarm gerekir. Tüketiciler ayrıca tekrarlı mesajlara dayanıklı olmalıdır; çünkü EventBridge ve çoğu broker en az bir kez teslim eder.
Geçiş Kestirmesi Olarak Ortak Veritabanı#
İki servisin aynı tabloyu okumasına izin vermek 2. fazı geçmenin en hızlı yolu, sonradan geri almanın ise en zor kararıdır; çünkü coupling kodda görünmez. Veriyi bir event’in arkasından kopyalayın, eski tabloyu yeni sahibi için salt okunur tutun ve faz kapanmadan ortak erişimi kaldırın.
Ne Zaman Ayrıştırmalı, Ne Zaman Kalmalı#
Bağlayıcı kısıt deploy çekişmesiyse ayrıştırın: birden fazla takım tek bir release trenini paylaşıyorsa, tek bir domain’in test suite’i herkesi bekletiyorsa ve çevresel bir feature’daki hata checkout’u da düşürebiliyorsa. Önce çevreyi çıkarın, iletişimi eventlere taşıyın; kalan çekirdek kendiliğinden küçülsün.
Tek bir takım kod tabanının tamamına sahipse, deploy’lar saatler değil dakikalar sürüyorsa ve acı deployment coupling’inden değil kod düzeninden geliyorsa monolit olarak kalın. Tek bir deployable içindeki modül sınırları aynı sorunu operasyonel maliyetin küçük bir kısmıyla çözer ve ileride çıkarma seçeneğini açık bırakır.
Kaynaklar#
- AWS Lambda nedir? (yeni sekmede açılır) - Monolit Express uygulamalarından olay güdümlü fonksiyonlara geçen ekipler için temel Lambda kavramları
- Amazon EventBridge nedir? (yeni sekmede açılır) - Ayrıştırılmış servislerin bağımsız iletişimini sağlayan event bus modeli
- AWS Ortamınızı Birden Fazla Hesapla Düzenleme (yeni sekmede açılır) - Mimari olgunlaştıkça servisleri hesaplar arasında izole etme rehberi
- Serverless Uygulamalar Lens - AWS Well-Architected (yeni sekmede açılır) - Monolitik iş yüklerini serverless servislere ayrıştırma kalıpları
- AWS Lambda fonksiyonları için en iyi uygulamalar (yeni sekmede açılır) - Stateless tasarım ve servis sınırları genelinde paylaşılan mutable state’den kaçınma
- AWS CDK v2 Geliştirici Rehberi (yeni sekmede açılır) - Çıkarılan microservislerin bağımsız dağıtımı için altyapı-kod yaklaşımı
İlgili yazılar
AWS Lambda, EventBridge ve DynamoDB ile pratik bir CQRS uygulaması; event sourcing, eventual consistency ve dağıtık sistemde debug yöntemleri.
architecture · dynamodb · event-driven +3
Lambda biçimli bir Node.js servisinde factory, service katmanı ve DI container'ı çoğu zaman fazladır. Yerlerine ne geçer, sınıflar nerede yerini korur?
event-driven · functional-programming · lambda +3
Kimlik okumaları DynamoDB'de kalsın; keyfi sıralama, filtre, facet ve tam metin arama zero-ETL OpenSearch okuma modeline gitsin. Bazen tek PostgreSQL ikisini de yener.
dynamodb · aws · architecture +2
Event odaklı sistemlerin arkasındaki zihinsel değişimi öğrenin: komut vermek yerine olguları duyurmak. İsimlendirme, ayrıştırma, nihai tutarlılık ve idempotency.
event-driven · messaging · idempotency +2
Bir UI parçasının arkasındaki ince sunum servisi yapışkan koda dönüşür. Port-ve-adaptör, çekirdeği somut hiçbir şeye bağımlı bırakmayarak bunu sürdürülebilir tutar.
architecture · nodejs · typescript +3