Skip to content

TypeScript Geliştiricilerin Monolitten Lambda'ya Taşıdığı Beş Anti-Pattern

DI container'lar, monolitik SDK'lar, god-handler'lar, modül üstü secret çağrıları ve ağır ORM'ler - soğuk başlatmada bedeli ve yerine geçen fonksiyonel yapı.

Problem

Ekipler NestJS, Spring veya .NET monolitlerinden AWS Lambda'ya geçerken uzun ömürlü servislerde işe yarayan kalıpları yanlarında getiriyor. Bundle'lar şişiyor, cold start uzuyor ve platformun ekonomisi geri tepiyor. Lambda bir mikroservis değil, fonksiyondur - monolit OO/DI kalıpları bundle'ı şişirir, cold start'ı uzatır.

Hasarın çoğunu beş alışkanlık üretiyor: DI container'lar, modüler olmayan AWS SDK import'ları, god-handler servis sınıfları, modül üstünde senkron secret çağrısı ve ağır ORM'ler. Aşağıdaki her bölüm semptomu adlandırır, maliyeti açıklar ve fonksiyonel alternatifi verir.

En belirgin gerilim dependency injection. NestJS'in serverless dokümantasyonu @nestjs/platform-fastify ile serverless-http kombinasyonunu desteklenen bir kalıp olarak sunar. AWS Lambda Best Practices ise deployment paketini şişiren framework'lerden kaçınılmasını ve handler'ların ince tutulmasını önerir. Her iki kaynak da yetkili; ancak maliyet asimetrisi tartışmalı değil. Uzun ömürlü bir serviste DI container milyonlarca isteğe yayılarak kendini amorti eder. Lambda'da ise her cold container kurulum maliyetini sıfırdan öder.

DI Container'lar (NestJS, tsyringe, InversifyJS)

Nasıl Görünür

Handler @Injectable ile dekore edilmiş, modül repository'leri, servisleri ve provider'ları bağlıyor, reflect-metadata her giriş noktasının üstünden import ediliyor. Handler, business logic'in ilk satırından önce controller'ı container üzerinden çözer.

Bedeli

reflect-metadata her cold start'a yüklenir. Decorator metadata'sı startup'ta üretilir. Container, handler tek bir bağımlılık kullansa bile tüm grafı baştan çözer. Bundle boyutu modüldeki her provider'ın maliyeti kadar büyür, yalnızca bu handler'ın kullandığı kadar değil. esbuild, container'ın çağıracağı constructor'ları tree-shake edemez.

Fonksiyonel Alternatif

Düz bir async fonksiyon export edin. Bağımlılıkları modül kapsamlı bir değişken içinde lazy şekilde kurun, ilk çağrıda başlatın, sonraki sıcak çağrılarda yeniden kullanın.

ts
// iyi: container yok, lazy singletonimport { S3Client } from "@aws-sdk/client-s3";import type { S3Event } from "aws-lambda";
let s3: S3Client | undefined;const getS3 = () => (s3 ??= new S3Client({}));
export const handler = async (event: S3Event) => {  const client = getS3();  // client kullan};

??= operatörü ilk çağrıda başlatır ve singleton'ı sıcak çağrılar arasında yeniden kullanır. Testler bağımlılıkları fonksiyon argümanı olarak geçer; bu da bir container'ı mock'lamaktan daha basittir.

Modüler Olmayan AWS SDK Import'ları

Nasıl Görünür

import * as AWS from 'aws-sdk' - Eylül 2024'te maintenance mode'a giren ve 2025-09-08'de end-of-support'a ulaşan v2 monolitik SDK. Güncel Node.js Lambda runtime'ları yerine @aws-sdk/* v3 paketlerini içerir.

Bedeli

v2 SDK her AWS servisini kapsayan tek büyük bir bundle. esbuild bir kısmını tree-shake edebilir ancak public yüzey ve paylaşılan iç yapı taban maliyeti yüksek tutar. v3 ise servis başına paketlere bölünmüştür ve middleware ayrı import edilir. Fark hem bundle boyutunda hem de INIT duration'da ölçülür.

Fonksiyonel Alternatif

Yalnızca kullandığınız client ve command'ları import edin. @aws-sdk/client-* paketlerinde kalın. esbuild ile bundle'layın, runtime'da gelen SDK'yi external olarak işaretleyin, gerisini tree-shaking yapsın.

ts
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
let s3: S3Client | undefined;const getS3 = () => (s3 ??= new S3Client({}));
export const handler = async (event: { bucket: string; key: string }) => {  const client = getS3();  const result = await client.send(    new GetObjectCommand({ Bucket: event.bucket, Key: event.key }),  );  return result.ContentLength;};

Build komutu runtime tarafından sağlanan SDK'yi external olarak işaretler; böylece bundle'da tekrar etmez:

bash
esbuild src/handler.ts \  --bundle \  --platform=node \  --target=node24 \  --external:@aws-sdk/* \  --minify \  --outfile=dist/handler.js

Her runtime sürümünde @aws-sdk/* paketlerinin runtime'a dahil olmaya devam ettiğini doğrulayın; AWS, runtime başına dahil edilen sürümleri yayımlar.

God-Handler Servis Sınıfları

Nasıl Görünür

Handler OrderService'e devreder; o OrderRepository'yi kullanır; o da Logger, MetricsClient ve DatabasePool'a bağımlıdır. Beşi de baştan kurulur çünkü monolit böyle yapar. Handler dosyası tek bir sınıf import eder ve üzerinde tek bir method çağırır.

Bedeli

Graftaki her bağımlılık cold start'ta constructor'ını çalıştırır. Handler erken çıksa bile bellek bunları fonksiyon ömrü boyunca tutar. Tree-shaking, runtime'ın çağıracağı constructor'ları eleyemez. Daha kötüsü, bu sınıflardan biri ağır bir transitif bağımlılık çekerse bundle bunu da miras alır.

Fonksiyonel Alternatif

Bir handler tek iş yapar. Çapraz kesişen ihtiyaçlar - logging, tracing, validation - Lambda Powertools veya Middy üzerinden middleware'e taşınır. Paylaşılan mantık, container'ın gezeceği bir grafa asılı sınıflara değil, handler başına import edilen saf fonksiyonlara taşınır.

ts
import { Logger } from "@aws-lambda-powertools/logger";import middy from "@middy/core";import jsonBodyParser from "@middy/http-json-body-parser";
const logger = new Logger({ serviceName: "orders" });
const baseHandler = async (event: { body: { id: string } }) => {  logger.info("processing order", { id: event.body.id });  // tek bir iş  return { statusCode: 200, body: JSON.stringify({ ok: true }) };};
export const handler = middy(baseHandler).use(jsonBodyParser());

Middy middleware'i düz fonksiyonlar olarak komposlar; Powertools utility'leri isimle import edildiğinde tree-shake edilebilir. Her iki yol da handler'ı küçük tutar.

Modül Üstünde Senkron Secret/SSM Çağrısı

Nasıl Görünür

ts
// kötü: her cold start'ı bloklarimport { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";import { Pool } from "pg";
const ssm = new SSMClient({});const secrets = await ssm.send(  new GetParameterCommand({ Name: "/db/url", WithDecryption: true }),);const db = new Pool({ connectionString: secrets.Parameter!.Value });
export const handler = async () => {  /* ... */};

Bedeli

Top-level await, INIT fazında çalışır ve INIT cold start'ın parçasıdır. Her cold container, handler başlamadan önce SSM veya Secrets Manager'a gidiş-dönüş bedelini öder. Ağ yavaşsa veya parameter store rate-limit'liyse billed duration büyür. INIT hataları, handler hataları gibi aynı şekilde yeniden denenmez.

Fonksiyonel Alternatif

İlk çağrıda lazy başlatın. Sonucu modül kapsamlı bir değişkende cache'leyin. Sıcak yollar için AWS Parameters and Secrets Lambda Extension, localhost HTTP endpoint arkasında TTL'li in-memory cache sağlar.

ts
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";import { Pool } from "pg";
let pool: Pool | undefined;
const getPool = async () => {  if (pool) return pool;  const ssm = new SSMClient({});  const result = await ssm.send(    new GetParameterCommand({ Name: "/db/url", WithDecryption: true }),  );  pool = new Pool({ connectionString: result.Parameter!.Value });  return pool;};
export const handler = async () => {  const db = await getPool();  // db kullan};

İlk çağrı gidiş-dönüşü öder; sıcak çağrılar atlar. Parameters and Secrets extension layer'ı eklendiğinde lookup yerel cache'e düşer ve gidiş-dönüş handler'ınızdan tamamen çıkar.

Ağır ORM'ler (Prisma, TypeORM, Mongoose)

Nasıl Görünür

Prisma client modül üstünden import edilmiş. prisma generate build'e dahil edilmiş. Query engine binary'si deployment paketine düşmüş. Metadata reflection ile TypeORM ise DI container ile aynı şekle sahiptir - startup'ta eager graf inşası. Mongoose ise modül yüklemesinde mongoose.connect(...) ve mongoose.model('Order', schema) çağrılarıyla bundle'daki her şemayı handler ihtiyaç duysun duymasın derler.

Bedeli

Prisma'nın query engine'i, client'ın bağlandığı ayrı bir binary. Bundle boyutu, runtime'ın @aws-sdk/* external numarasının çözemeyeceği megabyte'larla büyür. Cold start uzar çünkü engine ilk sorgudan önce başlatılır. TypeORM'in reflection adımı, DI container'larda reflect-metadata'nın ettiği gibi INIT'i bloklar. Mongoose, MongoDB driver'ını ve BSON parser'ını model kayıt defteriyle birlikte paketler; şema derleme ve plugin kaydı her cold start'ta modül yüklenirken çalışır, modül üstündeki mongoose.connect() ise dördüncü anti-pattern'in uyardığı INIT-fazı bloklamasını ekler.

Fonksiyonel Alternatif

DynamoDB şekilli iş için v3 SDK'nin DynamoDBDocumentClient yeterli. SQL için Kysely gibi ince bir query builder küçük gelir ve lazy yüklenir. MongoDB için resmi mongodb driver'ı tek başına birkaç yüz kilobyte; Mongoose'un sağladığı şema katmanı yerine Zod gibi küçük bir validator ile eşleyin ve bağlantıyı modül üstünde değil, modül kapsamlı bir değişken içinde lazy başlatın. Tam ORM ve ODM'leri, engine maliyetinin amorti olabileceği uzun ömürlü servislere saklayın.

ts
import { Kysely, PostgresDialect } from "kysely";import { Pool } from "pg";
interface Database {  orders: { id: string; total: number };}
let kysely: Kysely<Database> | undefined;
const getDb = () => {  return (kysely ??= new Kysely<Database>({    dialect: new PostgresDialect({      pool: new Pool({ connectionString: process.env.DB_URL }),    }),  }));};
export const handler = async (event: { id: string }) => {  const db = getDb();  return db    .selectFrom("orders")    .selectAll()    .where("id", "=", event.id)    .executeTakeFirst();};

Her fonksiyon bir ila üç tabloya dokunduğunda elle yazılmış SQL veya küçük bir builder kabul edilebilir. Yüzey bunun ötesine geçtiğinde, tasarımın verdiği sinyal şu sorudur: bu iş gerçekten Lambda'ya mı ait?

Default Ne Zaman Geçerli, Ne Zaman Override Edilir

Dallar override durumlarını adlandırır. Middy veya Powertools default'a eklenen bir şeydir, default'un yerine geçen değil. Aynı kaynak üzerinde iki üç sıkı ilişkili işlem yapan bir handler küçük bir router'ı paylaşabilir; erken bölmek başka yerde cold-start yüzeyi yaratır. Bu default'a uymayan override, paylaşılan validation'lı küçük bir CRUD seti - lambdalith ile Powertools orada makul. Default ise hâlâ ince fonksiyon.

Nasıl Ölçülür

İki sayı ince yapının çalışıp çalışmadığını söyler: esbuild sonrası bundle boyutu ve CloudWatch'tan INIT duration.

Bundle boyutu esbuild çıktı satırında görünür. Build'i çalıştırın ve byte sayısını okuyun:

bash
pnpm exec esbuild src/handler.ts \  --bundle \  --platform=node \  --target=node24 \  --external:@aws-sdk/* \  --minify \  --outfile=dist/handler.js

Tek amaçlı bir handler için 1 MB altını hedefleyin. Tree-shake edilmiş bir v3 client artı tipik business logic bunun çok altına iner.

INIT duration, CloudWatch'un her cold çağrı sonunda bastığı REPORT satırında çıkar:

REPORT RequestId: 8f5e... Duration: 12.34 ms Billed Duration: 13 ms Memory Size: 512 MB Max Memory Used: 67 MB Init Duration: 187.42 ms

Node.js için 200 ms altını hedefleyin. p99 cold start, Lambda Insights ve X-Ray'de ayrı raporlanır; sıcak p99'dan ayrı bir percentile olarak takip edin. Bağımsız çalışmalar (Lumigo, Datadog, AWS Compute Blog) Node.js cold start'ında bundle boyutunun ve modül üstü işin baskın iki etken olduğunu, bu sırada, tutarlı şekilde gösterir.

Kapanış

Lambda inceyi ödüllendirir. Her handler'ı lazy bağımlılıkları ve modüler import'ları olan tek tipli bir fonksiyon olarak ele alın. Çapraz kesişen ihtiyaçlar geldiğinde middleware'e (Powertools, Middy) uzanın; SQL kaçınılmaz olduğunda bir query builder'a uzanın. DI container'ları ve ağır ORM'leri, tasarlandıkları uzun ömürlü servislere saklayın.

Sınırı adlandırmak gerekirse: bu öneri route başına fonksiyon default'una uyar. Powertools'lu küçük bir CRUD lambdalith makul bir override; tam bir domain modeli ve repository grafı isteyen bir servis ise Lambda'yı değil ECS veya App Runner'ı istediğinizin sinyalidir.

Kaynaklar

İlgili Yazılar