Skip to content
~/sph.sh

DynamoDB Single-Table Design: Kapsamlı Modelleme Rehberi

DynamoDB single-table design'ı ilişkileri modelleme, GSI ve LSI seçimi, DAX optimizasyonu ve production NoSQL sistemlerinde yaygın hataları önleme konularında pratik örneklerle öğren.

Özet

Single-table design, DynamoDB için veri modelleme yaklaşımında temel bir değişimi temsil ediyor. Bu kapsamlı rehber, single-table pattern'leri ne zaman kullanacağını, one-to-one, one-to-many ve many-to-many ilişkileri nasıl modelleyeceğini, Global ve Local Secondary Index'ler arasındaki trade-off'ları, DAX caching entegrasyonunu ve pratik query optimizasyon tekniklerini ele alıyor. Çalışan TypeScript örnekleri, gerçek maliyet analizleri ve hot partition ile throttling sorunlarından kaçınmak için test edilmiş pattern'ler bulacaksın.

Single-Table Design Neden Önemli

DynamoDB ile çalışırken öğrendiğim en önemli ders, relational tablo mantığıyla düşünmenin sorun çözmekten çok sorun yarattığı oldu. Tipik yaklaşım - Users, Orders, Products için ayrı tablolar oluşturmak - birden fazla round-trip, karmaşık uygulama mantığı ve öngörülemeyen maliyetlere yol açıyor.

Single-table design, birden fazla entity type'ı generic partition ve sort key'ler kullanarak tek tabloda saklıyor. Bir customer'ı bir tablodan, order'larını başka bir tablodan çekmek yerine, her şeyi tek request'te alıyorsun. Bu sadece performansla ilgili değil; veri modellemeye yaklaşımını temelden değiştiriyor.

Temel İlkeler:

  1. Access pattern'ler önce: Schema'yı tasarlamadan önce her query'yi dokümante et
  2. Data locality: İlişkili veriyi aynı partition key kullanarak bir arada sakla
  3. Generic key'ler: Entity-specific isimler yerine PK ve SK kullan
  4. Item collection'lar: İlişkili item'ları shared partition key'lerle grupla
  5. Attribute overloading: Aynı attribute'lar farklı entity type'ları boyunca farklı amaçlara hizmet eder

Single-Table Design Ne Zaman Kullanılmalı

Single-table design, ilişkili veriyi birlikte çekmen gerektiğinde mükemmel çalışıyor. Ne zaman iyi çalıştığı hakkında öğrendiklerim:

İyi Kullanım Senaryoları:

  • Customer'ları order'larıyla birlikte çektiğin e-commerce sistemleri
  • Post'ları comment ve like'larla birlikte alan sosyal platformlar
  • Tenant isolation olan multi-tenant SaaS uygulamaları
  • Hierarchical veri içeren content management sistemleri
  • Cihaz bazında sensor reading'leri toplayan IoT platformları

Ne Zaman Kaçınmalı:

Ekiplerle çalışırken single-table design'ın her zaman doğru seçim olmadığını gördüm:

  • Ekipte DynamoDB uzmanlığı yok (öğrenme eğrisi gerçek)
  • Minimal ilişkili basit CRUD uygulamaları
  • Ad-hoc reporting ve data warehouse senaryoları
  • Tüm entity type'ları için strong consistency gerekiyor
  • Farklı entity'lerin tamamen farklı access pattern'leri var

Rick Houlihan'ın 2024 güncellemesi vurguluyor: "Birlikte erişilen şeyler birlikte saklanmalı." İlgisiz veriyi sadece bir pattern'i takip etmek için tek tabloya zorlama.

Partition Key ve Sort Key Stratejileri

Single-table design'ın temeli, key'lerini nasıl yapılandıracağını anlamakta yatıyor.

Partition Key Pattern'leri

typescript
// Entity type prefix - en yaygın patternPK: "CUSTOMER#123"PK: "ORDER#456"PK: "PRODUCT#789"
// Multi-tenant için composite partition keyPK: "TENANT#acme#USER#123"
// High-cardinality user-specific keyPK: "USER#${userId}" // Her user unique partition alır

Kaçınılması Gereken Anti-Pattern:

typescript
// Low-cardinality status key - hot partition yaratırPK: "STATUS#active" // Tüm active user'lar BİR partition'da// Partition başına 3,000 RCU limitine ulaştığında throttle olur

Sort Key Pattern'leri

Sort key'ler range query'leri ve hierarchical organization'ı mümkün kılıyor:

typescript
// Hierarchical sort key - prefix query'leri mümkün kılarSK: "US#CA#SanFrancisco#94102"// Query: begins_with(SK, "US#CA") tüm California item'larını döner
// Timestamp-based chronological orderingSK: "2024-01-15T10:30:00#EVENT#123"
// Version control patternSK: "v0_item123" // Current versionSK: "v1_item123" // Previous versionSK: "v2_item123" // Older version
// İlişkiler için compositeSK: "ORDERITEM#PRODUCT#789#2024-01-15"

Best Practice'ler:

  • High-cardinality partition key'ler kullan (userId, orderId, productId)
  • begins_with ve BETWEEN ile range query'leri destekleyecek sort key'ler tasarla
  • Chronological ordering için timestamp ekle
  • Entity type'ları boyunca consistent prefix'ler kullan

İlişkileri Modelleme

Her ilişki türünü çalışan örneklerle nasıl modelleyeceğini göstereyim.

One-to-One İlişkiler

İlişkili veriyi aynı item collection'da farklı sort key'lerle sakla:

typescript
interface User {  PK: string;  SK: string;  EntityType: "User";  email: string;  name: string;}
interface UserPreferences {  PK: string;  SK: string;  EntityType: "UserPreferences";  theme: "dark" | "light";  language: string;}
// Şöyle saklanır:{  PK: "USER#123",  SK: "METADATA",  EntityType: "User",  email: "[email protected]",  name: "John Doe"}{  PK: "USER#123",  SK: "PREFERENCES",  EntityType: "UserPreferences",  theme: "dark",  language: "en"}
// Tek query ikisini de getirirconst params = {  TableName: 'MainTable',  KeyConditionExpression: 'PK = :pk',  ExpressionAttributeValues: {    ':pk': 'USER#123'  }};

One-to-Many İlişkiler

Item collection'lar one-to-many ilişkileri basitleştiriyor:

typescript
interface Customer {  PK: string;  SK: string;  EntityType: "Customer";  name: string;  email: string;}
interface Order {  PK: string; // Customer PK ile aynı  SK: string; // Chronological sort key  EntityType: "Order";  orderId: string;  total: number;  status: string;  orderDate: string;}
// Customer{  PK: "CUSTOMER#123",  SK: "METADATA",  EntityType: "Customer",  name: "John Doe",  email: "[email protected]"}
// Bu customer için order'lar{  PK: "CUSTOMER#123",  SK: "ORDER#2024-01-15#456",  EntityType: "Order",  orderId: "456",  total: 99.99,  status: "delivered",  orderDate: "2024-01-15"}{  PK: "CUSTOMER#123",  SK: "ORDER#2024-01-20#457",  EntityType: "Order",  orderId: "457",  total: 149.99,  status: "pending",  orderDate: "2024-01-20"}
// Customer ve TÜM order'larını tek query'de alconst getCustomerWithOrders = async (customerId: string) => {  const result = await dynamodb.query({    TableName: 'MainTable',    KeyConditionExpression: 'PK = :pk',    ExpressionAttributeValues: {      ':pk': `CUSTOMER#${customerId}`    }  });
  return {    customer: result.Items?.find(item => item.SK === 'METADATA'),    orders: result.Items?.filter(item => item.SK.startsWith('ORDER#'))  };};
// Sadece pending order'ları alconst getPendingOrders = async (customerId: string) => {  const result = await dynamodb.query({    TableName: 'MainTable',    KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',    FilterExpression: '#status = :status',    ExpressionAttributeNames: {      '#status': 'status'    },    ExpressionAttributeValues: {      ':pk': `CUSTOMER#${customerId}`,      ':sk': 'ORDER#',      ':status': 'pending'    }  });
  return result.Items;};

Many-to-Many İlişkiler

Many-to-many ilişkileri modellemek için adjacency list pattern kullan:

typescript
interface ProductCategory {  PK: string;  SK: string;  EntityType: "ProductCategory";  productName?: string;  categoryName?: string;}
// Product birden fazla category'ye ait// Forward relationship'ler{  PK: "PRODUCT#789",  SK: "CATEGORY#Electronics",  EntityType: "ProductCategory",  categoryName: "Electronics"}{  PK: "PRODUCT#789",  SK: "CATEGORY#Gadgets",  EntityType: "ProductCategory",  categoryName: "Gadgets"}
// Reverse relationship'ler (bidirectional query için ikisini de yaz){  PK: "CATEGORY#Electronics",  SK: "PRODUCT#789",  EntityType: "CategoryProduct",  productName: "Wireless Headphones"}{  PK: "CATEGORY#Gadgets",  SK: "PRODUCT#789",  EntityType: "CategoryProduct",  productName: "Wireless Headphones"}
// Bir product için tüm category'leri query etconst getProductCategories = async (productId: string) => {  const result = await dynamodb.query({    TableName: 'MainTable',    KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',    ExpressionAttributeValues: {      ':pk': `PRODUCT#${productId}`,      ':sk': 'CATEGORY#'    }  });
  return result.Items;};
// Bir category'deki tüm product'ları query etconst getCategoryProducts = async (categoryName: string) => {  const result = await dynamodb.query({    TableName: 'MainTable',    KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',    ExpressionAttributeValues: {      ':pk': `CATEGORY#${categoryName}`,      ':sk': 'PRODUCT#'    }  });
  return result.Items;};

Trade-off: Her iki yönü yazmak write operation'larını ikiye katlar ama Scan operation'ı olmadan her iki yönde de efficient query'leri mümkün kılar.

Denormalization Pattern

Bazen sık erişilen veriye ekstra query olmadan ihtiyacın var:

typescript
interface Order {  PK: string;  SK: string;  EntityType: "Order";  orderId: string;  customerId: string;  // Denormalized customer data  customerName: string;  customerEmail: string;  total: number;}
{  PK: "ORDER#456",  SK: "METADATA",  EntityType: "Order",  orderId: "456",  customerId: "123",  customerName: "John Doe",      // Customer record'dan kopyalandı  customerEmail: "[email protected]", // Customer record'dan kopyalandı  total: 99.99}

Trade-off Analizi:

  • Daha hızlı read'ler: Customer detaylarını ayrıca fetch etmeye gerek yok
  • Daha karmaşık write'lar: Customer name güncellemesi tüm order'larını güncellemeyi gerektirir
  • Storage overhead: Customer verisi order'lar boyunca duplicate edilir
  • Eventual consistency: Customer data güncellemeleri order'ları güncellemek için background job gerektirir

Denormalization'ı read sıklığı write sıklığından önemli ölçüde fazla olduğunda ve veri nadiren değiştiğinde kullan.

GSI vs LSI: Doğru Seçim

Global Secondary Index'ler ile Local Secondary Index'ler arasındaki farkları anlamak efficient access pattern'ler için kritik.

Local Secondary Index (LSI)

LSI, base table ile partition key'i paylaşır ama farklı sort key kullanır:

typescript
// Table yapısıinterface Order {  PK: string;              // Partition key  SK: string;              // Sort key (order tarihi)  orderId: string;  status: string;  total: number;}
// Base table tarih bazında query yapar{  PK: "CUSTOMER#123",  SK: "ORDER#2024-01-15#456",  orderId: "456",  status: "delivered",  total: 99.99}
// LSI strong consistency ile status bazında query'i mümkün kılarconst lsiDefinition = {  IndexName: "LSI-Status",  KeySchema: [    { AttributeName: "PK", KeyType: "HASH" },    // Table ile aynı    { AttributeName: "status", KeyType: "RANGE" } // Farklı sort key  ],  Projection: {    ProjectionType: "ALL"  }};
// Customer'ın pending order'larını strong consistency ile query etconst params = {  TableName: 'Orders',  IndexName: 'LSI-Status',  KeyConditionExpression: 'PK = :pk AND #status = :status',  ExpressionAttributeNames: {    '#status': 'status'  },  ExpressionAttributeValues: {    ':pk': 'CUSTOMER#123',    ':status': 'pending'  },  ConsistentRead: true // Sadece LSI ile mümkün};

LSI Özellikleri:

  • Table creation'da tanımlanmalı (sonradan eklenemez)
  • Base table ile partition key paylaşır
  • Strongly consistent read'leri destekler
  • Base table ile throughput capacity paylaşır
  • Partition key value başına 10GB limit
  • Table başına maksimum 5 LSI
  • Ek capacity planning gerektirmez

Global Secondary Index (GSI)

GSI farklı partition ve sort key'ler kullanır, tamamen yeni access pattern'leri mümkün kılar:

typescript
// Table yapısıinterface Order {  PK: string;  SK: string;  EntityType: string;  orderId: string;  orderDate: string;  status: string;  GSI1PK: string;  // Tarih bazlı query'ler için  GSI1SK: string;}
{  PK: "CUSTOMER#123",  SK: "ORDER#456",  EntityType: "Order",  orderId: "456",  orderDate: "2024-01-15",  status: "delivered",  GSI1PK: "2024-01-15",        // Tarihe göre grupla  GSI1SK: "ORDER#456"}
// GSI Tanımıconst gsiDefinition = {  IndexName: "GSI1",  KeySchema: [    { AttributeName: "GSI1PK", KeyType: "HASH" },    { AttributeName: "GSI1SK", KeyType: "RANGE" }  ],  Projection: {    ProjectionType: "INCLUDE",    NonKeyAttributes: ["orderId", "status", "total"]  }};
// TÜM order'ları tarihe göre query et (tüm customer'lar için)const getOrdersByDate = async (date: string) => {  const result = await dynamodb.query({    TableName: 'MainTable',    IndexName: 'GSI1',    KeyConditionExpression: 'GSI1PK = :date',    ExpressionAttributeValues: {      ':date': date    }  });
  return result.Items;};

GSI Özellikleri:

  • Table creation'dan sonra eklenebilir veya çıkarılabilir
  • Base table'dan farklı partition ve sort key'ler
  • Sadece eventually consistent (strong consistency yok)
  • Provisioned mode'da independent throughput
  • Size limit'i yok
  • Table başına maksimum 20 GSI (5'ten artırıldı)
  • Cross-partition query'leri mümkün kılar

Karar Matrisi

GereksinimLSIGSI
Strong consistency gerekliYesNo
Farklı partition key gerekliNoYes
Table'dan sonra oluşturNoYes
Aynı partition, farklı sort orderYesYes
Independent capacity planningNoYes (provisioned)
Cross-partition query'lerNoYes
Item size > 10GB per partitionNoYes

LSI Ne Zaman Kullanılmalı:

  • Strong consistency gerektiğinde
  • Aynı partition'ı alternative sort order ile query ederken
  • Küçük dataset'ler (< 10GB per partition)
  • Access pattern'ler table creation'da bilindiğinde

GSI Ne Zaman Kullanılmalı:

  • Access pattern için farklı partition key gerektiğinde
  • Cross-partition query'ler gerektiğinde
  • Mevcut table'a yeni access pattern eklerken
  • Eventually consistent read'ler kabul edilebilir olduğunda
  • Büyük dataset'lerde

Sparse Index Pattern

Sadece GSI attribute'larına sahip item'lar index'e dahil edilir, storage maliyetlerini azaltır:

typescript
interface User {  PK: string;  SK: string;  EntityType: "User";  status: "active" | "inactive";  GSI1PK?: string; // Sadece active user'lar için set edilir  GSI1SK?: string;}
// Active user - index'te{  PK: "USER#123",  SK: "METADATA",  EntityType: "User",  status: "active",  email: "[email protected]",  GSI1PK: "ACTIVE_USERS",    // GSI'a dahil  GSI1SK: "USER#123"}
// Inactive user - index'te DEĞİL{  PK: "USER#456",  SK: "METADATA",  EntityType: "User",  status: "inactive",  email: "[email protected]"  // GSI1PK/GSI1SK yok - index'te değil, storage tasarrufu}
// Sadece active user'ları query etconst getActiveUsers = async () => {  const result = await dynamodb.query({    TableName: 'MainTable',    IndexName: 'GSI1',    KeyConditionExpression: 'GSI1PK = :type',    ExpressionAttributeValues: {      ':type': 'ACTIVE_USERS'    }  });
  return result.Items;};

Maliyet Tasarrufu: Sadece %10 user active ise, sparse indexing GSI storage'ı %90 azaltır.

DynamoDB Accelerator (DAX) Entegrasyonu

DAX, read-heavy workload'lar için in-memory caching ile microsecond response time'ları sağlıyor.

DAX Ne Zaman Kullanılmalı

typescript
// Senaryo 1: Read-heavy workload// E-commerce product catalog// - %95 read, %5 write// - Beklenen cache hit rate: %90+// - Fayda: 10x latency improvement + maliyet azaltma
// Senaryo 2: Hot key pattern// Flash sale - tek product saniyede 10,000 read alıyor// DAX olmadan: Throttling, yüksek maliyetler// DAX ile: Read'leri DynamoDB'den offload eder
// Senaryo 3: Tekrarlı read'ler// Aynı bölgesel veriyi tekrar tekrar query eden weather analizi// Analiz aynı dataset üzerinde saatlerce çalışıyor// DAX tüm dataset'i memory'de cache'liyor

DAX Ne Zaman KULLANILMAMALI

typescript
// Anti-pattern 1: Write-heavy workload// Sık güncellemeli real-time analytics// DAX fayda olmadan overhead ekler
// Anti-pattern 2: Strong consistency gerekli// Anında consistency gerektiren finansal transaction'lar// DAX sadece eventual consistency sağlar
// Anti-pattern 3: Düşük cache hit rate// Random access pattern'lı ad-hoc query'ler// Cache hit rate < %50 = kötü ROI
// Anti-pattern 4: Düşük trafik// Saniyede < 100 request alan uygulama// DAX maliyeti faydayı aşıyor

DAX Implementasyonu

typescript
import { DynamoDB } from '@aws-sdk/client-dynamodb';import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';import AmazonDaxClient from 'amazon-dax-client';
// DAX olmadan - direkt DynamoDBconst directClient = new DynamoDB({  region: 'us-east-1'});const dynamodb = DynamoDBDocument.from(directClient);
// DAX ileconst daxClient = new AmazonDaxClient({  endpoints: ['my-cluster.dax.us-east-1.amazonaws.com:8111'],  region: 'us-east-1'});const dax = DynamoDBDocument.from(daxClient);
// Aynı API - drop-in replacementconst getProduct = async (productId: string) => {  const params = {    TableName: 'Products',    Key: {      PK: `PRODUCT#${productId}`,      SK: 'METADATA'    }  };
  // İlk çağrı: DynamoDB query (5ms)  // Sonraki çağrılar: DAX cache (500μs)  const result = await dax.get(params);  return result.Item;};
// Query operation'ları da cache'lenirconst getProductReviews = async (productId: string) => {  const params = {    TableName: 'Products',    KeyConditionExpression: 'PK = :pk AND begins_with(SK, :sk)',    ExpressionAttributeValues: {      ':pk': `PRODUCT#${productId}`,      ':sk': 'REVIEW#'    }  };
  const result = await dax.query(params);  return result.Items;};

DAX Performance Metrikleri

typescript
const benchmarks = {  dynamodb: {    getItem: '3-5ms',    query: '5-10ms',    cost: 'Milyon read başına $0.155'  // Kasım 2024 güncellemesi  },  dax: {    getItem: '200-500μs (cache hit)',    query: '300-700μs (cache hit)',    cacheMiss: '4-6ms (DynamoDB + overhead)',    cost: 'Node başına saatte $0.11 (t3.small)'  }};

Maliyet Analizi

typescript
// Senaryo: %95 cache hit rate ile saniyede 1,000 request
// DAX olmadan:// - Request'ler: Ayda 2.59B// - DynamoDB maliyet: Ayda $401 (on-demand, $0.155/milyon)
// DAX ile (3-node t3.medium cluster):// - DAX cluster: Ayda $482// - Cache hit'ler (%95): 2.46B read (DAX tarafından servis edilir)// - Cache miss'ler (%5): DynamoDB'den 130M read// - DynamoDB maliyet: Ayda $20// - Toplam: Ayda $502// - Tasarruf: Bu ölçekte minimal; ana fayda 10x latency improvement
// Break-even noktası: %90+ cache hit rate ile ~saniyede 500 req// Ana değer: Sadece maliyet değil, latency azaltma

Query Optimizasyon Teknikleri

Query ve Scan operation'ları arasındaki fark performans ve maliyeti dramatik şekilde etkiliyor.

Query vs Scan

typescript
// ANTI-PATTERN: Scan operationconst findUserByEmail = async (email: string) => {  const result = await dynamodb.scan({    TableName: 'Users',    FilterExpression: 'email = :email',    ExpressionAttributeValues: {      ':email': email    }  });
  return result.Items?.[0];};// - Tüm table'ı okur, application'da filtreler// - 1M item = 1M RCU tüketir// - Latency: 5-60 saniye// - Maliyet: Scan başına $0.25
// BEST PRACTICE: GSI ile queryconst findUserByEmailOptimized = async (email: string) => {  const result = await dynamodb.query({    TableName: 'Users',    IndexName: 'GSI-Email',    KeyConditionExpression: 'GSI1PK = :email',    ExpressionAttributeValues: {      ':email': `EMAIL#${email}`    }  });
  return result.Items?.[0];};// - Direkt partition erişimi// - 1 item = 0.5 RCU (eventually consistent)// - Latency: 5-10ms// - Maliyet: Query başına $0.00000025 (1,000x daha ucuz)

Gerçek Dünya Etkisi

Milyonlarca user'lı sistemlerde çalışırken maliyet farkının dramatik olduğunu öğrendim:

typescript
// 1M user table'ında email ile user bulma
// Scan yaklaşımı:// - Read capacity: 1,000,000 RCU// - Süre: 45 saniye (pagination ile)// - Query başına maliyet: $0.25
// GSI ile query:// - Read capacity: 0.5 RCU// - Süre: 8ms// - Query başına maliyet: $0.00000025
// Sonuç: 1,000x maliyet azaltma, 5,000x daha hızlı

Projection Optimizasyonu

typescript
// ANTI-PATTERN: Tüm attribute'ları project etconst gsiAll = {  IndexName: 'GSI1',  Projection: {    ProjectionType: 'ALL' // Tüm item'ı duplicate eder  }};// Storage: 2x table size (table + full GSI copy)// Maliyet: Yüksek
// BEST PRACTICE: Sadece gerekli attribute'ları project etconst gsiInclude = {  IndexName: 'GSI1',  Projection: {    ProjectionType: 'INCLUDE',    NonKeyAttributes: ['name', 'email', 'status']  }};// Storage: Minimal (key'ler + belirtilen attribute'lar)// Maliyet: Optimize
// OPTIMAL: Full item'ı zaten fetch edeceksen sadece key'lerconst gsiKeys = {  IndexName: 'GSI1',  Projection: {    ProjectionType: 'KEYS_ONLY'  }};// Pattern kullan: GSI'dan ID'leri query et, sonra detaylar için BatchGetItem

Batch Operation'lar

typescript
// ANTI-PATTERN: Sequential GetItem çağrılarıconst getUsersSequential = async (userIds: string[]) => {  const users = [];
  for (const userId of userIds) {    const result = await dynamodb.get({      TableName: 'Users',      Key: {        PK: `USER#${userId}`,        SK: 'METADATA'      }    });    users.push(result.Item);  }
  return users;};// 100 user = 100 round trip = 500-1000ms
// BEST PRACTICE: BatchGetItemimport { BatchGetCommand } from '@aws-sdk/lib-dynamodb';
const getUsersBatch = async (userIds: string[]) => {  const command = new BatchGetCommand({    RequestItems: {      Users: {        Keys: userIds.map(id => ({          PK: `USER#${id}`,          SK: 'METADATA'        }))      }    }  });
  const result = await dynamodb.send(command);  return result.Responses?.Users || [];};// 100 user = 1 request (100 item'a kadar) = 50-100ms// 5-10x daha hızlı

Filtreleme için Composite Sort Key

typescript
// ANTI-PATTERN: Query + FilterExpressionconst getPendingOrdersWrong = async (customerId: string) => {  const result = await dynamodb.query({    TableName: 'Orders',    KeyConditionExpression: 'PK = :pk',    FilterExpression: '#status = :status',    ExpressionAttributeNames: {      '#status': 'status'    },    ExpressionAttributeValues: {      ':pk': `CUSTOMER#${customerId}`,      ':status': 'pending'    }  });
  return result.Items;};// TÜM order'ları okur, sonra filtreler// Tüm order'lar için RCU tüketir, sadece pending'leri döner
// BEST PRACTICE: Composite sort key// SK formatı: "STATUS#pending#ORDER#456"const getPendingOrdersOptimized = async (customerId: string) => {  const result = await dynamodb.query({    TableName: 'Orders',    KeyConditionExpression: 'PK = :pk AND begins_with(SK, :status)',    ExpressionAttributeValues: {      ':pk': `CUSTOMER#${customerId}`,      ':status': 'STATUS#pending'    }  });
  return result.Items;};// SADECE pending order'ları okur// Sadece eşleşen item'lar için RCU tüketir

Hot Partition'ları Önleme

Her DynamoDB partition 3,000 RCU ve 1,000 WCU destekler. Bu limitleri aşmak throttling'e neden olur.

Hot Partition Senaryoları

typescript
// ANTI-PATTERN 1: Low-cardinality partition key{  PK: "STATUS#active",  // Sadece 2-3 unique value  SK: "USER#123"}// Tüm active user'lar BİR partition'da// 3,000 RCU limitini kolayca aşar
// ANTI-PATTERN 2: Celebrity problemi{  PK: "USER#celebrity",  SK: "FOLLOWER#456"}// Bir partition'da milyonlarca follower// 10GB ve throughput limitlerini aşar
// ANTI-PATTERN 3: Sharding olmadan time-based key{  PK: "2024-01-15",  // Bugünün tüm verisi  SK: "EVENT#123"}// Peak saatlerde hot partition

Önleme Stratejisi 1: Write Sharding

typescript
// Write'ları dağıtmak için random suffix ekleconst SHARD_COUNT = 10;
const writeWithSharding = async (userId: string, data: any) => {  const shardId = Math.floor(Math.random() * SHARD_COUNT);
  await dynamodb.put({    TableName: 'Users',    Item: {      PK: `STATUS#active#${shardId}`, // 10 partition'a dağıtır      SK: `USER#${userId}`,      ...data    }  });};
// Okuma tüm shard'ları query etmeyi gerektirirconst getActiveUsers = async () => {  const promises = [];
  for (let i = 0; i < SHARD_COUNT; i++) {    promises.push(      dynamodb.query({        TableName: 'Users',        KeyConditionExpression: 'PK = :pk',        ExpressionAttributeValues: {          ':pk': `STATUS#active#${i}`        }      })    );  }
  const results = await Promise.all(promises);  return results.flatMap(r => r.Items || []);};
// Write'ları 10 partition'a dağıtır// Throughput: 10,000 WCU (1,000 * 10)

Önleme Stratejisi 2: Composite High-Cardinality Key'ler

typescript
// Low-cardinality'yi high-cardinality ile birleştir{  PK: `STATUS#active#USER#${userId}`, // User başına unique  SK: "METADATA"}
// Ya da low-cardinality query'ler için GSI kullan{  PK: `USER#${userId}`,  SK: "METADATA",  status: "active",  GSI1PK: "STATUS#active",  // Status query'leri için GSI  GSI1SK: `USER#${userId}`}

Önleme Stratejisi 3: Deterministik Sharding

typescript
import crypto from 'crypto';
const getShardId = (entityId: string, shardCount: number): number => {  const hash = crypto.createHash('md5').update(entityId).digest('hex');  return parseInt(hash.substring(0, 8), 16) % shardCount;};
const shardId = getShardId(userId, 10);const partitionKey = `USERS#${shardId}`;

Bu yaklaşım aynı entity'nin her zaman aynı shard'a gitmesini sağlar, tüm shard'ları query etmeden consistent read'leri mümkün kılar.

Maliyet Optimizasyon Stratejileri

Provisioned vs On-Demand

typescript
const pricing = {  provisioned: {    rcu: 'Saat başına $0.00013',    wcu: 'Saat başına $0.00065',    storage: 'GB-ay başına $0.25'  },  onDemand: {    readRequest: 'Milyon read başına $0.155',  // Kasım 2024 güncellemesi    writeRequest: 'Milyon write başına $0.78',  // Kasım 2024 güncellemesi    storage: 'GB-ay başına $0.25'  }};
// Örnek: Ayda 100M read, sabit trafik// Provisioned: ~38.5 RCU * $0.00013 * 730 saat = Ayda $3.65// On-Demand: 100M / 1M * $0.155 = Ayda $15.50// Fark: On-demand ile 4.25x daha pahalı

Provisioned Ne Zaman Kullanılmalı:

  • Öngörülebilir trafik pattern'leri
  • Yüksek volume (günde >1M request)
  • 7/24 production uygulamaları
  • Budget-conscious senaryolar
  • Reserved capacity'ye commit edilebilir (1-yıl %54 veya 3-yıl %77 tasarruf)

On-Demand Ne Zaman Kullanılmalı:

  • Öngörülemez trafik
  • Bilinmeyen yükü olan yeni uygulamalar
  • Development/testing ortamları
  • Spiky workload'lar (10x varyans)
  • Küçük ölçekli uygulamalar (günde 1M'den az request)

Sparse Index Tasarrufu

typescript
// Sparse index olmadan: Tüm 1M user index'te// Table: 10GB// ALL projection ile GSI: +10GB// Toplam: 20GB * $0.25 = Ayda $5 storage
// Sparse index ile: Sadece 100K active user index'te// Table: 10GB// Sparse index ile GSI: +1GB (user'ların %10'u)// Toplam: 11GB * $0.25 = Ayda $2.75 storage// Tasarruf: %45

Single-Table Maliyet Faydaları

typescript
// Multi-table yaklaşımı:// - Users: 5 RCU, 5 WCU// - Orders: 10 RCU, 10 WCU// - Products: 15 RCU, 5 WCU// - OrderItems: 20 RCU, 20 WCU// Toplam: 50 RCU, 40 WCU = Ayda $28.47
// Single-table yaklaşımı:// - MainTable: 30 RCU, 25 WCU// Toplam: Ayda $15.69// Tasarruf: %45 + basitleşmiş yönetim

Yaygın Hatalar ve Çözümler

Hata 1: Access Pattern'leri Önce Dokümante Etmemek

Deneyim gösteriyor ki query'leri anlamadan table tasarlamak yeniden tasarımlara yol açıyor:

typescript
// YANLIŞ yaklaşım:// 1. Entity'lere göre table'lar oluştur// 2. Query'ler çalışmadığında GSI ekle// 3. 5+ GSI ile bitir, hala inefficient
// DOĞRU yaklaşım:const accessPatterns = [  'Customer ve tüm order\'larını al',  'Order ve tüm item\'larını al',  'Tarih aralığına göre tüm order\'ları al',  'Email\'e göre customer al',  'Product ve tüm review\'larını al',  'Customer review\'larını al'];
// TÜM pattern'leri efficiently desteklemek için table ve index'leri tasarla

Hata 2: Throttling için Error Handling Eksikliği

typescript
// Production sorunu: Trafik spike sırasında retry logic yok// Sonuç: %50 error rate
// Çözüm: Exponential backoffimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({  region: 'us-east-1',  maxAttempts: 3,  retryMode: 'adaptive' // Built-in exponential backoff});
// Daha iyi: Circuit breaker pattern implementasyonu// En iyi: Hot partition'lardan kaçınacak şekilde tasarla

Hata 3: Item Size Limitlerini Görmezden Gelmek

typescript
// Problem: Item başına 400KB limit// Öğrenme: Bir order'da 500 satır item saklamak limiti aştı
// Çözüm 1: Pagination pattern{  PK: "ORDER#456",  SK: "ITEMS#PAGE#1",  items: [...] // İlk 100 item}{  PK: "ORDER#456",  SK: "ITEMS#PAGE#2",  items: [...] // Sonraki 100 item}
// Çözüm 2: Bireysel item'lar (tercih edilen){  PK: "ORDER#456",  SK: "ITEM#1",  productId: "789",  quantity: 2}// Her satır item'ı ayrı DynamoDB item olarak

Hata 4: Table Creation'da LSI Planlamama

Table creation'dan sonra LSI ekleyemezsin. Sonradan ihtiyacın olursa veri migration gerektirir:

typescript
// Hemen ihtiyacın olmasa bile LSI'ları önceden planlaconst tableDefinition = {  TableName: 'Orders',  KeySchema: [    { AttributeName: 'PK', KeyType: 'HASH' },    { AttributeName: 'SK', KeyType: 'RANGE' }  ],  LocalSecondaryIndexes: [    {      IndexName: 'LSI-Status',      KeySchema: [        { AttributeName: 'PK', KeyType: 'HASH' },        { AttributeName: 'status', KeyType: 'RANGE' }      ],      Projection: { ProjectionType: 'ALL' }    }  ]};
// GSI'lar sonradan eklenebilir, LSI'lar eklenemez

Hata 5: Yanlış Capacity Mode

typescript
// Production'ı on-demand ile başlattık// Trafik: Sabit 50 read/sec + 20 write/sec, 7/24// Aylık maliyet: ~$45/ay (AWS Calculator)// 129.6M reads + 51.84M writes
// Auto-scaling ile provisioned'a geçtik// 100 RCU + 40 WCU (eventually consistent reads)// Aylık maliyet: ~$28/ay// Tasarruf: ~38% (ayda $17)
// Ders: Öngörülebilir trafik için provisioned capacity daha uygun maliyetli// Değişken spike'lı trafik için on-demand tercih edilmeli

Single-Table Design NE ZAMAN Kullanılmamalı

Single-table design her zaman doğru seçim değil. Ne zaman kaçınmalı:

Ayrı Table'lar Ne Zaman Kullanılmalı:

  • Farklı entity type'ları çok farklı consistency gereksinimlerine sahip
  • Ekipte DynamoDB uzmanlığı yok (öğrenme eğrisi velocity'yi etkiler)
  • Minimal ilişkili basit CRUD (overhead gerekçelendirilemiyor)
  • Ad-hoc reporting ihtiyaçları (data warehouse pattern'leri daha uygun)
  • Microservice'lerde service boundary'leri (servis başına ayrı table'lar)
  • Farklı entity'lerin access pattern örtüşmesi yok

Rick Houlihan'ın 2024 rehberliği: "Configuration ve operational veriyi single table'da karıştırma. Service boundary'leri boyunca single table sürdürme."

Temel Çıkarımlar

DynamoDB single-table design ile çalışırken öğrendiklerim:

  1. Access pattern'ler önce: Schema tasarlamadan önce tüm query'leri dokümante et
  2. Data locality: İlişkili veriyi aynı partition key ile birlikte sakla
  3. Scan değil query: Her zaman Query operation'ları için tasarla (100-1000x daha ucuz)
  4. GSI vs LSI: Esneklik için GSI'lar, strong consistency için LSI'lar
  5. Hot partition'lar: High-cardinality partition key'ler kullan, gerektiğinde sharding implementasyonu
  6. DAX ROI: %90+ cache hit rate ile saniyede ~300 req'te break-even
  7. Maliyet optimizasyonu: Sabit trafik için provisioned mode (6-7x daha ucuz)
  8. Sparse index'ler: Subset'leri index'leyerek storage'da %50+ tasarruf
  9. Projection optimizasyonu: ALL yerine INCLUDE veya KEYS_ONLY kullan
  10. Limitleri bil: Item başına 400KB, LSI partition başına 10GB, partition başına 3,000 RCU

Type-Safe Implementation Kütüphaneleri

Single-table design pattern'lerini TypeScript ile implement ederken, raw AWS SDK yerine type-safe kütüphaneler kullanmak development hızını artırıyor ve runtime hatalarını önlüyor. İki popüler seçenek var:

DynamoDB Toolbox

DynamoDB Toolbox, AWS SDK v3 ile uyumlu, modern bir TypeScript kütüphanesi:

  • Type Safety: Entity tanımlarından otomatik TypeScript tipleri
  • Schema Validation: Runtime'da veri doğrulama
  • Query Builder: Type-safe query ve update expression'ları
  • Single-Table Desteği: GSI ve composite key pattern'leri için built-in support
  • AWS SDK v3: Son SDK versiyonu ile tam uyumluluk

Raw AWS SDK ile yazdığın karmaşık AttributeValue mapping'leri yerine, temiz ve bakımı kolay entity tanımları kullanabilirsin. Detaylı implementation örnekleri ve production best practice'leri için DynamoDB Toolbox rehberini incele.

OneTable

OneTable, single-table design için özel olarak tasarlanmış alternatif bir kütüphane:

  • Schema-Driven: JSON schema ile model tanımlama
  • Migration Support: Built-in schema migration desteği
  • TypeScript Generation: Schema'dan otomatik type generation
  • Developer Experience: Minimal boilerplate, sezgisel API
  • Validation: JSON Schema standardı ile güçlü validation

OneTable, özellikle büyük ve karmaşık single-table design'larda schema evolution ve migration ihtiyaçları için güçlü araçlar sunuyor.

Hangisini Seçmeli?

DynamoDB Toolbox tercih et:

  • AWS SDK v3'e migrate ediyorsan veya yeni başlıyorsan
  • AWS ekosistemi ile daha sıkı entegrasyon istiyorsan
  • Daha fazla AWS-native pattern kullanacaksan
  • Sitede detaylı rehber mevcut

OneTable tercih et:

  • Schema migration'lar sık yapıyorsan
  • JSON Schema standardını tercih ediyorsan
  • Daha fazla abstraction ve convention over configuration istiyorsan
  • Hızlı prototyping yapıyorsan

Her iki kütüphane de production-ready ve aktif olarak maintain ediliyor. Seçim, team preference ve proje gereksinimlerine bağlı.

İlgili Konular

DynamoDB ile çalışmak keşfetmeye değer birkaç alana bağlanıyor:

Single-table design, relational düşünmeden paradigma kaymasını temsil ediyor. Anahtar, önce access pattern'lerini anlamak, efficient query'leri destekleyecek key'ler tasarlamak ve workload'una uygun index'leri seçmek. Basit pattern'lerle başla, performansı ölç ve gerçek kullanıma göre tasarımını geliştir.

İlgili Yazılar