← Kursa Dön
📄 Text · 25 min

Twelve-Factor App

Giriş

2011 yılında Heroku'nun kurucu ortaklarından Adam Wiggins, modern web uygulamalarının nasıl geliştirilmesi gerektiğini 12 maddelik bir manifesto ile tanımladı. Bu manifesto başlangıçta PaaS (Platform as a Service) ortamları için yazılmış olsa da, bugün container'lar, Kubernetes ve mikroservis mimarilerinin temel referansı haline geldi.

Neden önemli? Çünkü bu 12 faktör, uygulamanızın "deploy edilebilir", "ölçeklenebilir" ve "bakımı yapılabilir" olmasını garanti eden mühendislik prensipleridir. Bunları uygulamayan bir yazılım, geliştirme ortamında mükemmel çalışsa bile production'da kabus yaşatır.

Bunu bir bina inşaatına benzetin: 12 faktör, binanın temeli, çelik konstrüksiyonu ve deprem yönetmeliğidir. Üzerine istediğiniz kadar güzel bir cephe koyabilirsiniz ama temel sağlam değilse bina çöker.

Bu derste her bir faktörü Spring Boot perspektifinden, gerçek kod örnekleriyle ve "neden?" sorusunu cevaplayarak derinlemesine inceleyeceğiz.

1. Codebase — Tek Kaynak Kod Deposu

İlke: Bir uygulama, bir kaynak kod deposu (repository) tarafından izlenir; birçok deploy yapılır.

Bir Repo (Git) ──▶ Development Deploy
                ──▶ Staging Deploy
                ──▶ Production Deploy

Ne Anlama Geliyor?

  • Her mikroservisin kendi Git repository'si olmalı (mono-repo tartışması ayrı bir konu)

  • Aynı repo'dan farklı ortamlara deploy edilebilir

  • Bir repo birden fazla uygulamayı barındırmamalı

  • Paylaşılan kod varsa library olarak çıkarılmalı (Maven/Gradle dependency)

# ❌ YANLIŞ — iki uygulama tek repo'da
my-monorepo/
├── user-service/
├── order-service/
└── shared-utils/

# ✅ DOĞRU — her servisin kendi repo'su
github.com/myorg/user-service
github.com/myorg/order-service
github.com/myorg/common-lib  → Maven Central'a publish
<!-- Paylaşılan kodu library olarak kullanma -->
<dependency>
    <groupId>com.myorg</groupId>
    <artifactId>common-lib</artifactId>
    <version>1.2.0</version>
</dependency>

Spring Boot'ta Uygulama

Spring Initializr ile oluşturulan her proje zaten bu prensibi uygular — tek bir pom.xml veya build.gradle, tek bir Application.java giriş noktası.

2. Dependencies — Açık Bağımlılık Beyanı

İlke: Tüm bağımlılıkları açıkça beyan edin ve izole edin. Sisteme kurulu bir kütüphaneye asla güvenmeyin.

<!-- pom.xml — tüm bağımlılıklar açık -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <!-- Versiyon BOM'dan gelir — kontrollü -->
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- Her bağımlılık AÇIKÇA belirtilmeli -->
</dependencies>

Neden Önemli?

# ❌ YANLIŞ — sisteme kurulu bir araca güvenmek
# "Sunucuda ImageMagick kurulu olmalı" → deployment'lar arası tutarsızlık

# ✅ DOĞRU — her şey proje içinde tanımlı
# Docker ile tüm bağımlılıklar container içinde
FROM eclipse-temurin:21-jre-alpine
# İhtiyaç duyulan her şey image içinde

Maven/Gradle, bağımlılıkları indirip izole bir classpath'te çalıştırır. Docker bunu bir üst seviyeye taşır — OS seviyesindeki bağımlılıkları da izole eder.

// build.gradle — Gradle ile
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'org.postgresql:postgresql'
    
    // Test bağımlılıkları açık
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.testcontainers:postgresql'
}

3. Config — Yapılandırmayı Ortamda Tutun

İlke: Deploy'lar arasında değişen her yapılandırma (veritabanı adresi, API key, feature flag) ortam değişkenlerinde tutulmalıdır. Koda gömmeyin.

Bu, 12 faktörün en sık ihlal edileni ve en önemlilerindendir.

# ❌ YANLIŞ — değerler kodda sabit
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: admin
    password: secret123

# ✅ DOĞRU — ortam değişkenlerinden
spring:
  datasource:
    url: ${DATABASE_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

Spring Boot'un Config Desteği

Spring Boot bu faktörü doğal olarak destekler:

// @Value ile ortam değişkeni
@Value("${app.feature.new-checkout:false}")
private boolean newCheckoutEnabled;

// @ConfigurationProperties ile type-safe
@ConfigurationProperties(prefix = "app")
public record AppConfig(
    String apiKey,
    int maxRetry,
    boolean debugMode
) {}
# application.yml — varsayılanlar
app:
  api-key: ${API_KEY:test-key}
  max-retry: ${MAX_RETRY:3}
  debug-mode: ${DEBUG_MODE:false}

Litmus Testi

Uygulamanızın kaynak kodunu şu anda open source yapsanız, herhangi bir credential ifşa olur mu? Eğer cevap "evet" ise, 3. faktörü ihlal ediyorsunuz.

4. Backing Services — Destek Servislerini Kaynak Olarak Bağlayın

İlke: Veritabanı, mesaj kuyruğu, cache, email servisi gibi backing service'leri takılıp çıkarılabilir (attached) kaynaklar olarak ele alın.

┌──────────────┐      ┌──────────────┐
│ Spring Boot  │─────▶│  PostgreSQL  │  ← Lokal veya RDS
│ Application  │─────▶│    Redis     │  ← Lokal veya ElastiCache
│              │─────▶│  RabbitMQ    │  ← Lokal veya CloudAMQP
└──────────────┘      └──────────────┘
        ↓
  Sadece connection string değişir, KOD değişmez
# Development — lokal servisler
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
  data:
    redis:
      host: localhost
      port: 6379

# Production — cloud servisler (sadece URL'ler değişir)
spring:
  datasource:
    url: jdbc:postgresql://rds.amazonaws.com:5432/mydb
  data:
    redis:
      host: elasticache.amazonaws.com
      port: 6379

Spring Boot'ta Uygulama

Spring Boot'un auto-configuration mekanizması bu prensibi doğal olarak destekler. spring.datasource.url değiştiğinde otomatik olarak yeni veritabanına bağlanır — kodda tek satır değişiklik gerekmez.

// Bu kod, PostgreSQL → MySQL → H2 geçişinde DEĞİŞMEZ
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

5. Build, Release, Run — Aşamaları Kesin Ayırın

İlke: Build (derleme), Release (sürüm) ve Run (çalışma) aşamaları birbirinden kesin ayrılmalıdır.

BUILD                    RELEASE                   RUN
┌────────────────┐      ┌────────────────┐      ┌────────────────┐
│ Kaynak Kodu    │      │ Build Artifact │      │ Çalışan        │
│ + Bağımlılıklar│─────▶│ + Config       │─────▶│ Process        │
│ → Artifact     │      │ → Release      │      │                │
└────────────────┘      └────────────────┘      └────────────────┘
  mvn package             Docker image +          java -jar
  → app.jar               env variables           kubectl apply
# Build — sürüm bağımsız artifact
./mvnw clean package -DskipTests
# Çıktı: target/myapp-1.0.0.jar

# Release — artifact + config
docker build -t myapp:1.0.0 .
# Image: myapp:1.0.0 + environment variables

# Run — çalıştır
docker run -e SPRING_PROFILES_ACTIVE=prod \
           -e DATABASE_URL=jdbc:... \
           myapp:1.0.0

Neden Önemli?

  • Build: Aynı kaynak kodu her zaman aynı artifact'ı üretmeli (reproducible build)

  • Release: Her release benzersiz bir ID'ye sahip, geri alınabilir

  • Run: Çalışma zamanında kod değişikliği yapılmamalı

# CI/CD Pipeline (GitHub Actions)
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: ./mvnw package -DskipTests
      - name: Docker Build & Push
        run: |
          docker build -t myapp:${{ github.sha }} .
          docker push myapp:${{ github.sha }}
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Production
        run: kubectl set image deployment/myapp myapp=myapp:${{ github.sha }}

6. Processes — Stateless (Durumsuz) Çalışın

İlke: Her uygulama instance'ı stateless olmalıdır. Oturum verileri, dosya upload'ları ve cache gibi state'ler harici servislere taşınmalıdır.

Bu prensip, yatay ölçeklemenin (horizontal scaling) ön koşuludur.

// ❌ YANLIŞ — state bellekte (bir instance'da)
@RestController
public class CartController {
    // Instance kapanırsa veya farklı instance'a yönlendirilirse
    // sepet kaybolur!
    private final Map<Long, List<CartItem>> carts = new HashMap<>();

    @PostMapping("/cart/{userId}/items")
    public void addItem(@PathVariable Long userId,
                        @RequestBody CartItem item) {
        carts.computeIfAbsent(userId, k -> new ArrayList<>()).add(item);
    }
}

// ✅ DOĞRU — state harici serviste (Redis)
@RestController
@RequiredArgsConstructor
public class CartController {
    private final RedisTemplate<String, List<CartItem>> redisTemplate;

    @PostMapping("/cart/{userId}/items")
    public void addItem(@PathVariable Long userId,
                        @RequestBody CartItem item) {
        String key = "cart:" + userId;
        List<CartItem> cart = redisTemplate.opsForValue().get(key);
        if (cart == null) cart = new ArrayList<>();
        cart.add(item);
        redisTemplate.opsForValue().set(key, cart,
            Duration.ofHours(24));
    }
}

Session Yönetimi

# ❌ YANLIŞ — in-memory session
# Varsayılan — session bilgisi JVM'de, farklı instance'a
# yönlendirilince session kaybolur

# ✅ DOĞRU — Redis-backed session
spring:
  session:
    store-type: redis
    redis:
      namespace: myapp:sessions
    timeout: 30m
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

Dosya Upload

// ❌ YANLIŞ — dosyayı yerel diske kaydetme
Files.copy(file.getInputStream(),
    Paths.get("/uploads/" + file.getOriginalFilename()));
// Instance değişirse dosya erişilemez!

// ✅ DOĞRU — S3 veya benzeri object storage
@Service
@RequiredArgsConstructor
public class FileService {
    private final S3Client s3Client;

    public String upload(MultipartFile file) {
        String key = UUID.randomUUID() + "-" + file.getOriginalFilename();
        s3Client.putObject(
            PutObjectRequest.builder()
                .bucket("my-uploads")
                .key(key)
                .build(),
            RequestBody.fromInputStream(
                file.getInputStream(), file.getSize())
        );
        return "https://my-uploads.s3.amazonaws.com/" + key;
    }
}

7. Port Binding — Kendi Portunu Bağla

İlke: Uygulama, kendi içinde HTTP sunucusu çalıştırarak kendini bir porta bağlar. Dışarıda bir web sunucusuna (Apache, Nginx) bağımlı değildir.

Spring Boot bunu zaten yapar — embedded Tomcat/Jetty/Undertow:

server:
  port: ${PORT:8080}  # Cloud platformları PORT env variable'ı sağlar
// Spring Boot uygulaması kendi Tomcat'ini içerir
@SpringBootApplication
public class MyApp {
    public static void main(String[] args) {
        SpringApplication.run(MyApp.class, args);
        // Tomcat 8080 portunda dinlemeye başlar
        // Harici web sunucusu gerekmez
    }
}

Geleneksel Java EE modelinde uygulamayı bir WAR olarak paketleyip Tomcat'e deploy ederdiniz. Spring Boot bu modeli tersine çevirir — uygulama kendi sunucusunu içerir.

8. Concurrency — Process Modeli ile Ölçeklendirin

İlke: Uygulamanızı process tipine göre ölçeklendirin. Tek bir büyük process yerine, birden fazla küçük process çalıştırın.

Web Requests    → Web Process (Tomcat threads)     × 3 instance
Background Jobs → Worker Process (Async threads)   × 2 instance
Scheduled Tasks → Clock Process                    × 1 instance
// Web process — HTTP isteklerini karşılar
@RestController
public class OrderController {
    // Tomcat thread pool ile ölçeklenir
}

// Worker process — async işleri yapar
@Component
public class OrderProcessor {
    @Async
    @RabbitListener(queues = "order-processing")
    public void processOrder(OrderEvent event) {
        // Ayrı process/pod olarak ölçeklenebilir
    }
}
# Kubernetes ile process tipine göre ölçekleme
# Web pods
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-web
spec:
  replicas: 3  # 3 web instance
  template:
    spec:
      containers:
        - name: myapp
          command: ["java", "-jar", "app.jar"]

# Worker pods
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-worker
spec:
  replicas: 2  # 2 worker instance
  template:
    spec:
      containers:
        - name: myapp
          command: ["java", "-jar", "app.jar", "--spring.main.web-application-type=none"]

9. Disposability — Hızlı Başla, Zarif Kapat

İlke: Uygulamalar hızlı başlamalı ve zarif (graceful) kapanmalıdır. Bu, hızlı deploy, ölçekleme ve kurtarma sağlar.

# Graceful shutdown
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Hızlı Başlama İçin

# Lazy initialization — bean'ler ilk kullanımda oluşturulur
spring:
  main:
    lazy-initialization: true  # Dikkat: production'da dikkatli kullan

# JVM startup optimizasyonu
# -XX:TieredStopAtLevel=1 (daha az JIT optimizasyonu, hızlı start)
# -Dspring.jmx.enabled=false (JMX kapalı = daha hızlı)
// Shutdown hook — temizlik
@Component
public class GracefulShutdown implements DisposableBean {
    @Override
    public void destroy() {
        // Devam eden işleri tamamla
        // Connection'ları kapat
        // Cache'i flush et
    }
}

10. Dev/Prod Parity — Ortam Farkını Minimize Edin

İlke: Development, staging ve production ortamları arasındaki farkı minimize edin.

Fark Türü         ❌ Geleneksel      ✅ 12-Factor
────────────────────────────────────────────────────
Zaman farkı       Haftalar/aylar     Saatler (CI/CD)
Personel farkı    Dev yazar,         Aynı kişi hem
                  ops deploy eder    yazar hem deploy eder
Araç farkı        Dev: H2,           Her yerde aynı:
                  Prod: PostgreSQL   PostgreSQL (Docker)
# Docker Compose ile dev ortamında gerçek servisleri kullanın
# docker-compose-dev.yml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_PASSWORD: devpassword
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "5672:5672"
      - "15672:15672"
// Testcontainers ile integration test'lerde gerçek veritabanı
@SpringBootTest
@Testcontainers
class OrderServiceIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
}

11. Logs — Stdout'a Yaz, Toplama Dışarıda

İlke: Log'ları dosyaya yazmayın, stdout/stderr'e yazın. Log toplama ve analiz, uygulama dışında yapılmalıdır.

# ❌ YANLIŞ — dosyaya yazmak
logging:
  file:
    name: /var/log/myapp/app.log  # Container yeniden başlarsa kaybolur

# ✅ DOĞRU — stdout'a yazmak (varsayılan)
logging:
  level:
    root: INFO
    com.example: DEBUG
# Log'lar stdout'a gider → Docker/K8s log driver toplar
# Docker log'ları
docker logs myapp-container

# Kubernetes log'ları
kubectl logs pod/myapp-xxx

# Log aggregation (ELK, Grafana Loki)
# stdout → Fluentd/Filebeat → Elasticsearch/Loki → Grafana/Kibana
<!-- Production'da JSON format — log aggregation araçlarıyla uyumlu -->
<springProfile name="prod">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
    </appender>
</springProfile>

12. Admin Processes — Tek Seferlik Görevleri Aynı Ortamda Çalıştırın

İlke: Database migration, data seed, one-off script gibi yönetim görevlerini aynı codebase ve ortamda çalıştırın.

// Spring Boot CommandLineRunner ile admin process
@Component
@Profile("migration")  // Sadece migration profile'ında çalışır
public class DataMigrationRunner implements CommandLineRunner {

    private final UserRepository userRepository;

    @Override
    public void run(String... args) {
        // Eski formattaki verileri yeni formata dönüştür
        userRepository.findAll().forEach(user -> {
            if (user.getFullName() == null) {
                user.setFullName(user.getFirstName() + " " + user.getLastName());
                userRepository.save(user);
            }
        });
        log.info("Migration complete");
    }
}
# Admin process olarak çalıştırma
java -jar app.jar --spring.profiles.active=migration

# Kubernetes Job olarak
kubectl create job data-migration --image=myapp:1.0.0 \
    -- java -jar app.jar --spring.profiles.active=migration

# Flyway migration — uygulama başlatılırken otomatik çalışır
# (Faktör 12'nin en yaygın Spring Boot uygulaması)

Spring Boot ve 12 Faktör Uyum Tablosu

FaktörSpring Boot DesteğiNasıl?
1. Codebase✅ DoğalSpring Initializr, tek proje yapısı
2. Dependencies✅ DoğalMaven/Gradle, BOM
3. Config✅ Mükemmel@Value, @ConfigurationProperties, profiles
4. Backing Services✅ MükemmelAuto-configuration, connection string değişikliği
5. Build/Release/Run✅ Doğalmvn package → Docker → java -jar
6. Processes⚠️ Dikkat gerekirRedis session, S3 file storage — manuel
7. Port Binding✅ DoğalEmbedded Tomcat
8. Concurrency⚠️ Dikkat gerekirK8s replicas, async processing — manuel
9. Disposability✅ İyiserver.shutdown=graceful
10. Dev/Prod Parity⚠️ Dikkat gerekirDocker Compose, Testcontainers — manuel
11. Logs✅ Doğalstdout varsayılan
12. Admin Processes✅ İyiCommandLineRunner, Flyway

Yaygın İhlaller ve Çözümler

İhlal 1: Secret'lar Kodda

# ❌ password: mySecret123
# ✅ password: ${DB_PASSWORD}

İhlal 2: Dosyaya Log Yazmak

# ❌ logging.file.name: /var/log/app.log
# ✅ stdout'a yaz, dışarıda topla

İhlal 3: In-Memory Session

# ❌ Session JVM'de → ölçeklenemez
# ✅ spring.session.store-type: redis

İhlal 4: Dev'de H2, Prod'da PostgreSQL

# ❌ Farklı veritabanları → farklı davranışlar
# ✅ Docker Compose ile dev'de de PostgreSQL

İhlal 5: Manuel Deploy

# ❌ ssh prod-server && git pull && mvn package && restart
# ✅ CI/CD pipeline → otomatik build → otomatik deploy

Özet

  • Codebase: Bir repo, çok deploy — paylaşılan kodu library yapın

  • Dependencies: pom.xml / build.gradle ile tüm bağımlılıkları açıkça tanımlayın

  • Config: Ortam değişkenleri kullanın — kod içi secret OLMAZ

  • Backing Services: Veritabanı, cache, queue = takılıp çıkarılabilir kaynak

  • Build/Release/Run: mvn package → Docker image → java -jar — aşamaları karıştırmayın

  • Processes: Stateless çalışın — state Redis/S3'te, session Redis'te

  • Port Binding: Embedded Tomcat — harici web sunucusu gerekmez

  • Concurrency: Process modeli ile ölçekleyin (web × 3, worker × 2)

  • Disposability: Hızlı başla (lazy-init), zarif kapat (graceful shutdown)

  • Dev/Prod Parity: Docker Compose + Testcontainers — dev ile prod arasında fark yok

  • Logs: stdout'a yaz, toplama ve analiz dışarıda (ELK/Loki)

  • Admin Processes: Migration, seed, script → aynı codebase, CommandLineRunner veya Flyway