Java ve Spring Boot ile Master Proje: Elly Mimarisi
Spring Boot 3.5 + Java 21 üzerine kurulu Elly CMS projesinin derinlemesine backend mimarisi: multitenancy, RBAC, Redis, RabbitMQ, OAuth2, JWT, Zipkin dağıtık izleme ve MapStruct.
Bu makalede, geliştirdiğim elly projesi etrafında şekillenen Backend (Arka Yüz) mimarisinin zorluklarını ve çözüm yollarını sizlerle paylaşıyorum. Bir projenin tek tıkla ayağa kalkması, milyonlarca veriyi saniyeler içinde işleyebilmesi ve çoklu müşteri (multitenant) bir yapıda tek JAR ile çalışabilmesinin mimari gerekliliklerini Spring Boot 3.5.7 + Java 21 ekosistemi üzerinden anlatacağım.
1. Elly Backend Mimarisine Genel Bakış
elly, tamamen Mikroservis esnekliklerine açık, ancak katmanlı monolit yapısının birleşimi gibi hareket eden modern bir Spring Boot API'sidir. Paket yapısını incelediğinizde klasik "layered architecture" prensiplerine sadık kaldığını göreceksiniz:
com.cms
├── advice # @RestControllerAdvice ile global exception handling
├── config # Spring, Security, Redis, RabbitMQ, OpenAPI konfigürasyonları
├── controller # REST endpoint'ler
├── dto # Request / Response DTO sınıfları
├── elly # Tenant-aware core modül
├── entity # JPA Entity'ler (Hibernate 6, JSONB dahil)
├── enums # Rol, Durum, Queue isimleri vb. sabitler
├── exception # Custom BusinessException, NotFoundException, UnauthorizedException
├── filter # JwtAuthenticationFilter, TenantContextFilter
├── mapper # MapStruct ile Entity ↔ DTO dönüşümü
├── repository # Spring Data JPA repository'leri
├── service # İş kuralları, transaction yönetimi
└── util # Slug, date, crypto, JSONB helper'ları
Mimaride hedeflenen noktalar:
- %100 Güvenlik (Role-Based Access Control + OAuth2 + JWT)
- Tenant-Based System (tenant1, tenant2 ve basedb ile izole veritabanları)
- Hızlı tepki süreleri (Redis AOF + LRU eviction)
- Asenkron görev işleyici (RabbitMQ üzerinden Email Worker)
- Observability (Actuator + Prometheus + Zipkin distributed tracing)
2. Güvenlik ve RBAC (Role-Based Access Control)
Kullanıcıların ne yapabileceğini belirleyen yetki hiyerarşisi (RBAC) tasarladık. Spring Security + spring-boot-starter-oauth2-client üzerine JJWT (jjwt-api, jjwt-impl, jjwt-jackson) ile token üretip doğrulayarak stateless bir authentication akışı kurduk. @PreAuthorize anotasyonunu method seviyesine indirgeyerek, endpoint'te hangi kullanıcının izni olduğunu veritabanındaki rolleriyle yönetebiliyoruz.
@PreAuthorize("hasRole('ADMIN') or hasAuthority('POST_PUBLISH')")
@PostMapping("/{id}/publish")
public ResponseEntity<PostResponse> publish(@PathVariable UUID id) {
return ResponseEntity.ok(postService.publish(id));
}Ancak burada devasa bir problem doğuyor: Her istekte 3 SQL sorgusu atmak!
Userstablosu kontrolüRolesvePermissionsjoin'leriUser_Tenantsilişkisi
Request başına bu 3 sorgu, saniyede 1000 req alan bir endpoint'te DB'yi anlık 3000 query'ye boğuyor. Çözümü filter/JwtAuthenticationFilter.java içinde Redis cache ile entegre ederek çözdük — ayrıntıları bir sonraki bölümde.
OAuth2 Sosyal Giriş
spring-boot-starter-oauth2-client sayesinde Google / GitHub girişlerini de aynı JWT akışı içinde eritiyoruz. Ingress üzerinde /login/oauth2 ve /oauth2 path'leri özel olarak route ediliyor; callback sonrası kullanıcı resolve edilip bizim kendi JWT'miz issue ediliyor.
3. Performans Kurtarıcısı: Redis Cache
Yukarıdaki yoğun SQL sorgularını çözmek adına araya Redis 7 (Alpine) soktum. spring-boot-starter-cache + spring-boot-starter-data-redis bileşenini kullanarak @Cacheable, @CachePut ve @CacheEvict anotasyonlarını tenant-aware çalışacak şekilde sardım.
Önemli tasarım detayları:
- Tenant-prefix key scheme:
tenantId::cacheName::keyformatıyla tek Redis instance üzerinde onlarca tenant'ı birbirine karıştırmadan tutuyorum. - AOF (Append-Only File) persistence:
--appendonly yesile restart sonrası cache'i koruyorum; cold-start senaryosunda DB'ye ani yük binmesini engelliyor. - LRU eviction:
maxmemory 256mb+allkeys-lrupolitikası — cache şişerse en az kullanılanı düşürüyor. - CachedUserDetails DTO:
JwtAuthenticationFilteriçinde her istekte DB yerine Redis'ten user + rol + permission bilgisini alıyorum. DB'ye inmeden, saniyenin binde biri sürelerde yetkilendirme tamamlanıyor. - Event-driven invalidation: Bir kullanıcının rolü değiştiğinde Spring'in
ApplicationEventPublisher'ı ile event yayınlanıyor; ilgili key'ler Redis'ten siliniyor.
@Cacheable(value = "user-permissions", key = "#tenantId + '::' + #userId")
public CachedUserDetails loadCachedPermissions(UUID tenantId, UUID userId) {
return userRepository.findWithRolesAndPermissions(userId)
.map(mapper::toCachedDetails)
.orElseThrow(() -> new NotFoundException("User not found"));
}4. Multitenancy: tenant1, tenant2 ve basedb
k8s/2c-postgres.yaml manifesto'sunda üç ayrı PostgreSQL 16 StatefulSet göreceksiniz: tenant1, tenant2 ve basedb. Her biri 8Gi PVC ile kendi verisini izole ediyor.
Spring tarafında bu yapıyı destekleyebilmek için:
filter/TenantContextFilter— Host header veya JWTtidclaim'inden tenant kimliğini okuyarakThreadLocaltenant context oluşturuyor.AbstractRoutingDataSource— Spring'in native mekanizması iledetermineCurrentLookupKey()içinden tenant context'ini DataSource seçimine çeviriyor.- Hibernate
MultiTenancyStrategy.DATABASE— aynı JAR, aynı entity'ler; ama farklı schema/database üzerinde çalışıyor. - Flyway ya da elle yazılmış
init.sqlscript'leri — her tenant DB'si kendi migration'ını çalıştırıyor.
Bu yaklaşım sayesinde tek bir JAR deploy ederek N tane müşteriyi izole şekilde yönetebiliyorum.
5. SMTP Sorunları ve RabbitMQ Asenkron İşlemleri
elly projesi kapsamında çoklu müşteri (tenant-based) mimarisinden ötürü, Spring'in varsayılan JavaMailSender konfigürasyonunu kapatıp runtime'da veritabanından dinamik olarak okunan SMTP hesaplarını devreye aldım. MAIL_ACCOUNT_PANEL_GUIDE.md dokümanında yönetim paneli üzerinden SMTP hesabı ekleme akışı detaylı anlatılıyor.
Kullanıcı kayıt olduğunda ya da email onayı gerektiğinde sisteme binen yük, Java'nın synchronous akışını tamamen yavaşlatır. Bu yüzden spring-boot-starter-amqp ile RabbitMQ 3.13 Management Alpine Message Broker'ı devreye aldım:
@RabbitListener(queues = RabbitQueues.EMAIL_OUTBOUND, concurrency = "3-10")
public void consumeEmail(EmailMessage msg) {
try {
mailService.send(msg);
} catch (MailException ex) {
retryHandler.scheduleDelayedRetry(msg, ex);
}
}- İstek
elly.email.outboundqueue'sunaCorrelationIdile atılır. - Arka plandaki Consumer'lar (3-10 concurrency) Thymeleaf template'i render edip SMTP'ye gönderim dener.
- K8s ortamında geçici SMTP bağlantı retleri (
connection refused) ile karşılaşmıştık.Delayed Retry Mechanism+ Dead Letter Exchange (DLX) kombinasyonu ile başarısız gönderimlerin sonsuz bir sıkı döngü (tight loop) yaratmasını engelledik. 3 deneme sonrası mesajelly.email.dlqkuyruğuna düşer, manuel inceleme için Management UI'da görünür.
6. JSONB + Hibernate 6
Postgres'in en sevdiğim özelliği olan JSONB desteğini hypersistence-utils-hibernate-63 kütüphanesi ile entity'lere katıyorum. CMS projelerinin olmazsa olmazı olan "dinamik alanlar" (örneğin bir Post'un meta field'ları, SEO alanları, custom widget verileri) için kolonu esnek tutabiliyorum:
@Entity
public class Post {
@Id
private UUID id;
@Type(JsonBinaryType.class)
@Column(columnDefinition = "jsonb")
private Map<String, Object> metadata;
}Ardından Postgres tarafında CREATE INDEX idx_post_meta ON post USING GIN (metadata); ile JSON içindeki anahtarlar üzerinde hızlı aramalar yapabiliyorum.
7. MapStruct + Lombok: Boilerplate'e Veda
Entity ↔ DTO dönüşümünü elle yazmak hem hataya açık hem sıkıcı. MapStruct compile-time'da bu sınıfları üretiyor; Lombok ise @Getter, @Builder, @RequiredArgsConstructor ile boilerplate'i siliyor. İkisini kullanırken lombok-mapstruct-binding dependency'sini unutmamak gerekiyor — yoksa Lombok getter/setter'ları MapStruct tarafından görünmüyor.
8. Observability: Actuator + Micrometer + Zipkin
elly'nin en önemli farklarından biri gözlemlenebilirlik katmanının başından itibaren kurulmuş olması:
spring-boot-starter-actuator→/actuator/health,/actuator/metrics,/actuator/prometheusendpoint'leri.micrometer-registry-prometheus→ Metric'leri Prometheus formatında expose eder.micrometer-tracing-bridge-brave+zipkin-reporter-brave→ Her HTTP request'etraceId+spanIdatar, RabbitMQ consumer'larına propagate eder ve Zipkin'e raporlar.
Dockerfile'daki HEALTHCHECK bile wget http://localhost:8080/actuator/health üzerine kurulu — yani K8s liveness/readiness probe'ları ile tam uyumlu.
9. OpenAPI / Swagger UI
springdoc-openapi-starter-webmvc-ui sayesinde /swagger-ui.html ve /api-docs endpoint'leri otomatik açılır. Ingress konfigürasyonunda bu path'lerin route edildiğini de göreceksiniz:
/api, /swagger-ui, /api-docs, /actuator, /login/oauth2, /oauth2, /assets, /
Bu sayede frontend ekibine ayrı Postman koleksiyonu göndermek yerine "buradan interaktif olarak dene" diyebiliyorum.
10. Global Exception Handling
advice/GlobalExceptionHandler içinde @RestControllerAdvice ile:
MethodArgumentNotValidException→ 400 + field-level validation mesajlarıBusinessException→ 409 + custom error codeNotFoundException→ 404AccessDeniedException→ 403Exception→ 500 + trace-id ekleyerek (Zipkin ile eşleşiyor) logluyor
Tüm hatalar tek bir ErrorResponse şemasına oturuyor, frontend tarafı da bunu tek bir interceptor ile yakalayabiliyor.
Sonuç
elly projesi; güçlü Java/Spring Boot backend dinamikleri ile modern "Cloud Native" araçlarının harmanlandığı oldukça keyifli bir platform hâline geldi:
- Verilerin PostgreSQL 16 üzerinde tenant bazlı izole tutulduğu,
- Asenkron yüklerin RabbitMQ ile azaltıldığı,
- Saniyelik statik verilerin Redis + AOF + LRU ile şahlandığı,
- OAuth2 + JWT + RBAC ile sıkı güvenlik katmanlarına sahip,
- Actuator + Prometheus + Zipkin ile tam gözlemlenebilir,
tam donanımlı bir DevOps & Backend harikasıdır. Bir sonraki yazıda bu yapının Kubernetes'te nasıl ayağa kaldırıldığını detaylıca inceleyeceğiz.