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 DeployNe 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çindeMaven/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: 6379Spring 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.0Neden Ö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: 30sHı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ör | Spring Boot Desteği | Nasıl? |
|---|---|---|
| 1. Codebase | ✅ Doğal | Spring Initializr, tek proje yapısı |
| 2. Dependencies | ✅ Doğal | Maven/Gradle, BOM |
| 3. Config | ✅ Mükemmel | @Value, @ConfigurationProperties, profiles |
| 4. Backing Services | ✅ Mükemmel | Auto-configuration, connection string değişikliği |
| 5. Build/Release/Run | ✅ Doğal | mvn package → Docker → java -jar |
| 6. Processes | ⚠️ Dikkat gerekir | Redis session, S3 file storage — manuel |
| 7. Port Binding | ✅ Doğal | Embedded Tomcat |
| 8. Concurrency | ⚠️ Dikkat gerekir | K8s replicas, async processing — manuel |
| 9. Disposability | ✅ İyi | server.shutdown=graceful |
| 10. Dev/Prod Parity | ⚠️ Dikkat gerekir | Docker Compose, Testcontainers — manuel |
| 11. Logs | ✅ Doğal | stdout varsayılan |
| 12. Admin Processes | ✅ İyi | CommandLineRunner, 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.gradleile tüm bağımlılıkları açıkça tanımlayınConfig: 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ınProcesses: 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,
CommandLineRunnerveya Flyway
AI Asistan
Sorularını yanıtlamaya hazır