Hüseyin DOLHüseyin DOL
Java ve Spring Boot ile Master Proje: Elly Mimarisi
Backend

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.

Hüseyin DOL
Hüseyin DOL
14 dk okuma

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!

  • Users tablosu kontrolü
  • Roles ve Permissions join'leri
  • User_Tenants iliş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::key formatıyla tek Redis instance üzerinde onlarca tenant'ı birbirine karıştırmadan tutuyorum.
  • AOF (Append-Only File) persistence: --appendonly yes ile restart sonrası cache'i koruyorum; cold-start senaryosunda DB'ye ani yük binmesini engelliyor.
  • LRU eviction: maxmemory 256mb + allkeys-lru politikası — cache şişerse en az kullanılanı düşürüyor.
  • CachedUserDetails DTO: JwtAuthenticationFilter iç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:

  1. filter/TenantContextFilter — Host header veya JWT tid claim'inden tenant kimliğini okuyarak ThreadLocal tenant context oluşturuyor.
  2. AbstractRoutingDataSource — Spring'in native mekanizması ile determineCurrentLookupKey() içinden tenant context'ini DataSource seçimine çeviriyor.
  3. Hibernate MultiTenancyStrategy.DATABASE — aynı JAR, aynı entity'ler; ama farklı schema/database üzerinde çalışıyor.
  4. Flyway ya da elle yazılmış init.sql script'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.outbound queue'suna CorrelationId ile 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ı mesaj elly.email.dlq kuyruğ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/prometheus endpoint'leri.
  • micrometer-registry-prometheus → Metric'leri Prometheus formatında expose eder.
  • micrometer-tracing-bridge-brave + zipkin-reporter-brave → Her HTTP request'e traceId + spanId atar, 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 code
  • NotFoundException → 404
  • AccessDeniedException → 403
  • Exception → 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.