← Kursa Dön
📄 Text · 30 min

@Async Anotasyonu

Giriş

Bir şirket düşünün: CEO her departmanla birebir ilgileniyor — önce muhasebe, sonra pazarlama, sonra İK. Tüm gün sürer. Akıllı CEO ise toplantıda kararları verir ve uygulama görevlerini departmanlara delege eder. Kendisi bir sonraki konuya geçerken, departmanlar kendi işlerini paralel olarak yapar. Spring'deki @Async anotasyonu tam olarak bu "akıllı CEO" modelidir — bir metodu çağırdığınızda işi başka bir thread'e delege eder, siz hemen bir sonraki işe geçersiniz.

Spring Framework'te bir metodu asenkron olarak çalıştırmanın en kolay yolu @Async anotasyonudur. Bu anotasyon, metodu çağıran thread'i bloke etmeden, metodun ayrı bir thread'de çalışmasını sağlar. Arka planda Spring, AOP (Aspect-Oriented Programming) mekanizmasını kullanarak metot çağrısını bir proxy üzerinden yönlendirir ve belirtilen TaskExecutor üzerinde çalıştırır.

Bu derste @Async'in nasıl etkinleştirildiğini, void ve dönüş değerli kullanımlarını, proxy mekanizmasının nasıl çalıştığını, self-invocation tuzağını, exception handling stratejilerini, @Transactional ile birlikte kullanımını ve production best practice'lerini derinlemesine öğreneceğiz.


@EnableAsync ile Aktivasyon

@Async anotasyonunun çalışabilmesi için uygulamanın asenkron desteğini açıkça etkinleştirmesi gerekir. Bu adım atlanırsa @Async sessizce görmezden gelinir — hata almaz ama metot senkron çalışır.

// Yöntem 1: Ayrı konfigürasyon sınıfı (önerilen)
@Configuration
@EnableAsync
public class AsyncConfig {
    // @Async artık aktif!
}

// Yöntem 2: Ana uygulama sınıfında (basit projeler için)
@SpringBootApplication
@EnableAsync
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

@EnableAsync'in yaptığı şey: Spring context'inde bir AsyncAnnotationBeanPostProcessor kaydeder. Bu processor, @Async anotasyonlu metot içeren bean'leri tespit eder ve onları proxy ile sarar.

@EnableAsync Parametreleri

@EnableAsync(
    annotation = Async.class,          // Hangi anotasyonu tarayacak (varsayılan: @Async)
    proxyTargetClass = false,          // true → CGLIB proxy, false → JDK dynamic proxy
    mode = AdviceMode.PROXY,           // PROXY veya ASPECTJ
    order = Ordered.LOWEST_PRECEDENCE  // AOP advice sırası
)

💡 İpucu: proxyTargetClass = true yaparsanız CGLIB proxy kullanılır. Bu, interface olmayan sınıfları da proxy'leyebilir. Spring Boot varsayılan olarak CGLIB kullanır.


Temel Kullanım: void Metotlar (Fire-and-Forget)

En basit ve en yaygın senaryo: dönüş değeri olmayan asenkron metotlar. "Yap ve unut" — sonucuyla ilgilenmiyorsunuz.

@Service
@Slf4j
public class NotificationService {
    
    @Async
    public void sendEmail(String to, String subject, String body) {
        log.info("E-posta gönderiliyor: thread={}", Thread.currentThread().getName());
        try {
            emailClient.send(to, subject, body);
            log.info("E-posta başarıyla gönderildi: to={}", to);
        } catch (Exception e) {
            log.error("E-posta gönderilemedi: to={}", to, e);
            // Exception çağırana İLETİLMEZ — kaybolur!
            // AsyncUncaughtExceptionHandler ile yakalanmalı
        }
    }
    
    @Async
    public void sendSms(String phone, String message) {
        log.info("SMS gönderiliyor: thread={}", Thread.currentThread().getName());
        smsClient.send(phone, message);
    }
    
    @Async
    public void sendPushNotification(String userId, String title, String body) {
        log.info("Push gönderiliyor: thread={}", Thread.currentThread().getName());
        pushClient.send(userId, title, body);
    }
}

Çağıran tarafta:

@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final NotificationService notificationService;
    private final PaymentService paymentService;
    private final InventoryService inventoryService;
    
    public OrderResult processOrder(Order order) {
        // 1. Senkron — Kritik iş mantığı
        PaymentResult payment = paymentService.charge(order);
        inventoryService.reserve(order);
        
        // 2. Asenkron — Fire-and-forget bildirimler
        // Bu üç metot AYNI ANDA, farklı thread'lerde çalışır
        notificationService.sendEmail(order.getEmail(), "Sipariş Onayı", "...");
        notificationService.sendSms(order.getPhone(), "Siparişiniz alındı");
        notificationService.sendPushNotification(order.getUserId(), "Sipariş", "...");
        
        // 3. Bildirimler arka planda devam ederken HEMEN dönüyoruz
        // Kullanıcı bildirim gönderimleri bitmeden yanıtını alır
        return new OrderResult(payment, "SUCCESS");
    }
}

Log çıktısı:

10:30:45 [http-nio-8080-exec-1] OrderService - Sipariş işleniyor
10:30:47 [http-nio-8080-exec-1] OrderService - Ödeme alındı, bildirimler gönderiliyor
10:30:47 [async-1] NotificationService - E-posta gönderiliyor: thread=async-1
10:30:47 [async-2] NotificationService - SMS gönderiliyor: thread=async-2
10:30:47 [async-3] NotificationService - Push gönderiliyor: thread=async-3
10:30:47 [http-nio-8080-exec-1] OrderService - Yanıt döndürülüyor (bildirimler devam ediyor)
10:30:49 [async-1] NotificationService - E-posta gönderildi
10:30:48 [async-2] NotificationService - SMS gönderildi
10:30:48 [async-3] NotificationService - Push gönderildi

Dikkat edin: http-nio-8080-exec-1 (istek thread'i) hemen dönerken, async-1, async-2, async-3 thread'leri arka planda çalışmaya devam ediyor.


Dönüş Değeri: Future ve CompletableFuture

Asenkron metodun sonucuna ihtiyacınız varsa, Future<T> veya CompletableFuture<T> dönüş tipi kullanırsınız:

Future (Eski Yöntem)

@Service
public class ReportService {
    
    @Async
    public Future<Report> generateReport(String type) {
        Report report = heavyReportGeneration(type); // 30 saniye sürebilir
        return new AsyncResult<>(report); // Spring'in Future wrapper'ı
    }
}

// Çağıran tarafta
Future<Report> future = reportService.generateReport("SALES");

// Seçenek 1: Bloke ederek bekle
Report report = future.get();  // Tamamlanana kadar BLOKE OLUR

// Seçenek 2: Timeout ile bekle
Report report = future.get(30, TimeUnit.SECONDS);  // 30 saniye timeout

// Seçenek 3: Hazır mı kontrol et
if (future.isDone()) {
    Report report = future.get();
}

CompletableFuture (Önerilen)

@Service
public class ReportService {
    
    @Async
    public CompletableFuture<Report> generateReportAsync(String type) {
        Report report = heavyReportGeneration(type);
        return CompletableFuture.completedFuture(report);
    }
}

// Çağıran tarafta — non-blocking zincir
reportService.generateReportAsync("SALES")
    .thenApply(report -> enrichReport(report))       // Raporu zenginleştir
    .thenAccept(report -> emailService.send(report))  // E-posta gönder
    .exceptionally(ex -> {
        log.error("Rapor oluşturulamadı", ex);
        return null;
    });

Birden Fazla Asenkron Sonucu Birleştirme

@RestController
@RequiredArgsConstructor
public class DashboardController {
    
    private final DashboardService dashboardService;
    
    @GetMapping("/dashboard/{userId}")
    public DashboardResponse getDashboard(@PathVariable Long userId) {
        // 3 servis PARALEL çağrılır
        CompletableFuture<UserProfile> profileFuture = 
            dashboardService.fetchProfile(userId);
        CompletableFuture<List<Order>> ordersFuture = 
            dashboardService.fetchOrders(userId);
        CompletableFuture<WalletInfo> walletFuture = 
            dashboardService.fetchWallet(userId);
        
        // Hepsi tamamlanana kadar bekle
        CompletableFuture.allOf(profileFuture, ordersFuture, walletFuture).join();
        
        return new DashboardResponse(
            profileFuture.join(),   // Artık bloke etmez (zaten tamamlandı)
            ordersFuture.join(),
            walletFuture.join()
        );
    }
}

@Service
public class DashboardService {
    
    @Async
    public CompletableFuture<UserProfile> fetchProfile(Long userId) {
        UserProfile profile = userClient.getProfile(userId); // 200ms
        return CompletableFuture.completedFuture(profile);
    }
    
    @Async
    public CompletableFuture<List<Order>> fetchOrders(Long userId) {
        List<Order> orders = orderClient.getOrders(userId); // 150ms
        return CompletableFuture.completedFuture(orders);
    }
    
    @Async
    public CompletableFuture<WalletInfo> fetchWallet(Long userId) {
        WalletInfo wallet = walletClient.getWallet(userId); // 100ms
        return CompletableFuture.completedFuture(wallet);
    }
}

Senkron: 200 + 150 + 100 = 450ms → Asenkron: max(200, 150, 100) = 200ms 🚀


Proxy Mekanizması — @Async Nasıl Çalışır?

@Async anotasyonu, Spring AOP proxy mekanizmasına dayanır. Bu mekanizmayı anlamadan @Async kullanmak, kaçınılmaz olarak hatalara yol açar.

Akış

Çağıran kod → Proxy (intercept) → TaskExecutor.submit() → Yeni Thread → Gerçek metot

Spring, @Async annotasyonlu metot içeren bean'i bir proxy ile sarar. Dışarıdan bu bean'in bir metodu çağrıldığında:

  1. Çağrı önce proxy'ye gider

  2. Proxy, @Async anotasyonunu kontrol eder

  3. Metodu bir TaskExecutor üzerinden submit eder (ayrı thread)

  4. Çağırana hemen null (void) veya CompletableFuture döner

  5. Gerçek metot arka planda çalışır

Self-Invocation Tuzağı ⚠️

Bu mekanizmanın en kritik sonucu: aynı sınıf içinden çağrı proxy'yi bypass eder!

@Service
public class MyService {
    
    @Async
    public void asyncMethod() {
        log.info("Bu asenkron çalışmalı — ama çalışacak mı?");
    }
    
    public void callerMethod() {
        asyncMethod();  // ⚠️ Bu SENKRON çalışır!
        // Çünkü this.asyncMethod() proxy'yi bypass eder
        // this → gerçek nesne (proxy değil)
    }
}

Neden? callerMethod() çağrıldığında akış proxy üzerinden geçer. Ancak callerMethod içindeki asyncMethod() çağrısı this.asyncMethod() şeklinde çalışır — buradaki this proxy değil, gerçek nesnedir.

Çözüm 1: Ayrı Servis (En İyi)

@Service
public class AsyncTaskService {
    
    @Async
    public void asyncMethod() {
        log.info("Asenkron çalışıyor! thread={}", Thread.currentThread().getName());
    }
}

@Service
@RequiredArgsConstructor
public class MyService {
    
    private final AsyncTaskService asyncTaskService;
    
    public void callerMethod() {
        asyncTaskService.asyncMethod();  // ✅ Proxy üzerinden geçer → asenkron çalışır
    }
}

Çözüm 2: Self-Injection

@Service
public class MyService {
    
    @Autowired
    private MyService self;  // Proxy'yi inject et (dikkatli kullanın!)
    
    @Async
    public void asyncMethod() {
        log.info("Asenkron çalışıyor!");
    }
    
    public void callerMethod() {
        self.asyncMethod();  // ✅ Proxy üzerinden geçer
    }
}

Çözüm 3: ApplicationContext

@Service
public class MyService implements ApplicationContextAware {
    
    private ApplicationContext context;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.context = applicationContext;
    }
    
    @Async
    public void asyncMethod() {
        log.info("Asenkron çalışıyor!");
    }
    
    public void callerMethod() {
        context.getBean(MyService.class).asyncMethod();  // ✅ Proxy üzerinden
    }
}

💡 İpucu: En temiz yaklaşım Çözüm 1'dir. Asenkron metotları ayrı bir servis sınıfına taşımak, Single Responsibility prensibine de uygundur.


Asenkron Metotlarda Exception Handling

void Metotlarda — Exception Kaybolur!

void dönüş tipli asenkron metotlarda fırlatılan exception'lar sessizce kaybolur. Çağıran thread artık o metotla ilgilenmez — exception'ı yakalayacak kimse yoktur.

@Async
public void riskyOperation() {
    throw new RuntimeException("Bu hata nereye gider?");
    // Cevap: HİÇBİR YERE! Sessizce kaybolur.
    // Log bile yazılmaz (kendi try-catch'iniz yoksa)
}

Bu durumu yönetmek için AsyncUncaughtExceptionHandler kullanılır:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, params) -> {
            log.error("🚨 Async exception — method: {}.{}, message: {}",
                method.getDeclaringClass().getSimpleName(),
                method.getName(),
                throwable.getMessage(),
                throwable);
            
            // Metrik kaydet
            meterRegistry.counter("async.errors",
                "method", method.getName(),
                "exception", throwable.getClass().getSimpleName()
            ).increment();
            
            // Kritik hatalarda alert gönder
            if (throwable instanceof CriticalException) {
                alertService.sendAlert("Async Critical Error: " + throwable.getMessage());
            }
        };
    }
    
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}

CompletableFuture Metotlarda — Exception Future İçinde

@Async
public CompletableFuture<Report> generateReport(String type) {
    try {
        Report report = heavyGeneration(type);
        return CompletableFuture.completedFuture(report);
    } catch (Exception e) {
        CompletableFuture<Report> failed = new CompletableFuture<>();
        failed.completeExceptionally(e);
        return failed;
    }
}

// Çağıran tarafta — exception yakalanabilir
reportService.generateReport("SALES")
    .thenAccept(report -> process(report))
    .exceptionally(ex -> {
        log.error("Rapor oluşturulamadı: {}", ex.getMessage());
        alertService.notify("Report generation failed");
        return null;
    });

@Async + @Transactional Birlikteliği

@Async ve @Transactional birlikte kullanıldığında dikkat edilmesi gereken önemli noktalar vardır.

Temel Kural

// ❌ YANLIŞ — @Async metot yeni thread'de çalışır, çağıranın transaction'ına KATILMAZ
@Async
@Transactional
public void updateOrderStatus(Long orderId, OrderStatus status) {
    // Bu metot KENDİ transaction'ını açar (yeni thread = yeni transaction)
    // Çağıranın transaction'ı ile ilişkisi YOKTUR
    Order order = orderRepository.findById(orderId).orElseThrow();
    order.setStatus(status);
    orderRepository.save(order);
}

// Çağıran tarafta
@Transactional
public void processOrder(Order order) {
    orderRepository.save(order);
    
    // Bu asenkron metot farklı thread'de, FARKLI transaction'da çalışır
    // Ana transaction rollback olsa bile bu değişiklik GERİ ALINMAZ
    updateOrderStatus(order.getId(), OrderStatus.PROCESSING);
}

Doğru Kullanım

@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final AsyncNotificationService asyncNotificationService;
    
    @Transactional
    public Order processOrder(OrderRequest request) {
        // 1. Ana iş mantığı — aynı transaction
        Order order = orderRepository.save(createOrder(request));
        orderItemRepository.saveAll(createItems(order, request));
        
        // 2. Transaction commit'lendikten SONRA asenkron işlem
        // @TransactionalEventListener(phase = AFTER_COMMIT) kullanmak daha güvenli
        return order;
    }
}

// Transaction commit'lendikten sonra asenkron bildirim
@Component
public class OrderCreatedHandler {
    
    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderCreated(OrderCreatedEvent event) {
        // Transaction başarıyla commit'lendikten SONRA çalışır
        // Rollback olursa bu metot ÇALIŞMAZ
        notificationService.sendConfirmation(event.getOrder());
    }
}

Belirli Executor Kullanma

Farklı iş yükleri için farklı executor'lar tanımlayıp @Async anotasyonunda belirtebilirsiniz:

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean(name = "emailExecutor")
    public TaskExecutor emailExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(5);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix("email-");
        executor.initialize();
        return executor;
    }
    
    @Bean(name = "reportExecutor")
    public TaskExecutor reportExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(3);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.setThreadNamePrefix("report-");
        executor.initialize();
        return executor;
    }
}

@Service
public class NotificationService {
    
    @Async("emailExecutor")  // email thread pool'unu kullan
    public void sendEmail(String to, String body) { /* ... */ }
    
    @Async("reportExecutor") // report thread pool'unu kullan
    public CompletableFuture<Report> generateReport(String type) { /* ... */ }
}

⚠️ Dikkat: @Async ile executor adı belirtmezseniz, varsayılan TaskExecutor bean'i kullanılır. Eğer hiçbir executor tanımlanmamışsa Spring, SimpleAsyncTaskExecutor kullanır — bu, her çağrıda yeni thread oluşturur ve production için uygun DEĞİLDİR!


Yaygın Hatalar

1. @EnableAsync Unutmak

// ❌ @EnableAsync yok — @Async sessizce görmezden gelinir
@Service
public class MyService {
    @Async
    public void doWork() {
        // Bu SENKRON çalışır! Hata almaz, sadece beklediğiniz gibi çalışmaz.
    }
}

2. Private Metotta @Async

@Service
public class MyService {
    
    @Async
    private void secretWork() {
        // ❌ ÇALIŞMAZ — Proxy, private metotları intercept edemez
    }
    
    @Async
    protected void protectedWork() {
        // ❌ ÇALIŞMAZ — JDK dynamic proxy ile (CGLIB ile çalışabilir ama önerilmez)
    }
    
    @Async
    public void publicWork() {
        // ✅ Sadece public metotlar desteklenir
    }
}

3. Yanlış Dönüş Tipi

@Service
public class MyService {
    
    @Async
    public String getResult() {
        // ❌ YANLIŞ — String dönemez!
        // @Async metotlar: void, Future<T> veya CompletableFuture<T> dönmeli
        return "result";  // Bu değer kaybolur, çağıran null alır
    }
    
    @Async
    public CompletableFuture<String> getResultAsync() {
        // ✅ DOĞRU
        return CompletableFuture.completedFuture("result");
    }
}

4. SecurityContext ve MDC Kaybı

@Async
public void processWithUser() {
    // ❌ SecurityContext KAYBOLUR — farklı thread!
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    // auth = null veya Anonymous
    
    // ❌ MDC değerleri de kaybolur — farklı thread!
    String requestId = MDC.get("requestId");
    // requestId = null
}

// ✅ ÇÖZÜM: TaskDecorator ile context propagation
@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setTaskDecorator(new ContextCopyingDecorator());
        executor.initialize();
        return executor;
    }
}

public class ContextCopyingDecorator implements TaskDecorator {
    
    @Override
    public Runnable decorate(Runnable runnable) {
        // Ana thread'den context'leri yakala
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        SecurityContext securityContext = SecurityContextHolder.getContext();
        Map<String, String> mdcContext = MDC.getCopyOfContextMap();
        
        return () -> {
            try {
                // Yeni thread'e context'leri aktar
                if (requestAttributes != null) {
                    RequestContextHolder.setRequestAttributes(requestAttributes);
                }
                SecurityContextHolder.setContext(securityContext);
                if (mdcContext != null) {
                    MDC.setContextMap(mdcContext);
                }
                
                runnable.run();
            } finally {
                // Temizle (thread reuse nedeniyle zorunlu)
                RequestContextHolder.resetRequestAttributes();
                SecurityContextHolder.clearContext();
                MDC.clear();
            }
        };
    }
}

@Async Kuralları Özet Tablosu

KuralAçıklama
@EnableAsync zorunluKonfigürasyon sınıfında açıkça etkinleştirilmeli
public metot olmalıPrivate/protected metotlarda çalışmaz
Self-invocation çalışmazAynı sınıf içinden çağrı proxy'yi bypass eder
Dönüş tipivoid, Future<T> veya CompletableFuture<T> olmalı
Exception handlingvoid metotlarda AsyncUncaughtExceptionHandler kullanın
Transaction@Async metot kendi transaction'ını açar, çağıranınkine katılmaz
SecurityContextFarklı thread'e aktarılmaz, TaskDecorator gerekir
MDCFarklı thread'e aktarılmaz, TaskDecorator gerekir
ExecutorBelirtilmezse varsayılan kullanılır; production'da özel executor tanımlayın

Gerçek Dünya Örneği: Kullanıcı Kayıt Akışı

@Service
@RequiredArgsConstructor
@Slf4j
public class UserRegistrationService {
    
    private final UserRepository userRepository;
    private final AsyncOnboardingService onboardingService;
    private final ApplicationEventPublisher eventPublisher;
    
    @Transactional
    public User register(RegistrationRequest request) {
        // 1. Validasyon ve kayıt (senkron — kritik)
        validateUniqueEmail(request.getEmail());
        User user = userRepository.save(User.from(request));
        
        // 2. Event yayınla — listener'lar asenkron çalışır
        eventPublisher.publishEvent(new UserRegisteredEvent(user));
        
        log.info("Kullanıcı kaydedildi: {} — onboarding arka planda başlıyor", user.getId());
        return user;
    }
}

@Component
@Slf4j
public class UserOnboardingListener {
    
    @Async("onboardingExecutor")
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onUserRegistered(UserRegisteredEvent event) {
        User user = event.getUser();
        log.info("Onboarding başlıyor: userId={}", user.getId());
        
        // Bu işlemler arka planda, paralel çalışır
        try {
            welcomeEmailService.send(user);
            log.info("Welcome e-postası gönderildi: {}", user.getEmail());
        } catch (Exception e) {
            log.error("Welcome e-postası gönderilemedi", e);
        }
        
        try {
            avatarService.generateDefault(user);
            log.info("Varsayılan avatar oluşturuldu");
        } catch (Exception e) {
            log.error("Avatar oluşturulamadı", e);
        }
        
        try {
            analyticsService.trackSignup(user);
            log.info("Signup analitik kaydedildi");
        } catch (Exception e) {
            log.error("Analitik kaydedilemedi", e);
        }
    }
}

Bu yapıda kullanıcı kaydı milisaniyeler içinde tamamlanır. Hoşgeldin e-postası, avatar oluşturma ve analitik kaydı arka planda paralel çalışır. Herhangi biri başarısız olsa bile kullanıcı kaydı etkilenmez.


Özet

  • @Async anotasyonu, bir metodu ayrı thread'de çalıştırmanın en kolay yoludur. @EnableAsync ile aktifleştirilmeli, aksi halde sessizce görmezden gelinir.

  • void metotlar fire-and-forget senaryolar için, CompletableFuture sonuca ihtiyaç duyulan senaryolar için kullanılır. Future<T> eski usuldür, CompletableFuture<T> tercih edin.

  • @Async, Spring AOP proxy mekanizmasına dayanır. Self-invocation (aynı sınıf içinden çağrı) proxy'yi bypass eder ve metot senkron çalışır. Çözüm: asenkron metodu ayrı servise taşıyın.

  • void metotlarda exception sessizce kaybolur. AsyncUncaughtExceptionHandler ile global hata yakalama tanımlayın. CompletableFuture'da .exceptionally() kullanın.

  • @Async metot kendi thread'inde çalıştığı için SecurityContext, MDC ve RequestAttributes aktarılmaz. TaskDecorator ile context propagation yapın.

  • Production'da mutlaka özel executor tanımlayın. Varsayılan SimpleAsyncTaskExecutor her çağrıda yeni thread oluşturur — bellek tükenmesine yol açar.