Bir uygulamadaki tek bir iş akışı zamanla onlarca farklı servise dokunmaya başladığında kodun kontrolünü kaybetmek çok kolaydır.
Bir havayolu rezervasyonunu düşünün. Rezervasyonu oluşturmak için önce doğrulama yapmanız gerekir. Sonra ödeme alınır, rezervasyon database’e kaydedilir, bilet PDF’i oluşturulur, bagaj sistemine bilgi gönderilir, müşteriye mail veya SMS gider ve belki müşteriye sadakat puanı eklenir.
Başlangıçta bunların hepsini tek bir yerde çağırmak kolay görünür. Ama zamanla şu hale gelir:
BookingService
├── BookingCheckPipeline
├── PaymentGateway
├── BookingRepository
├── TicketPdfGenerator
├── BaggageSystemClient
├── NotificationService
└── LoyaltyPointsService
Buradaki problem bu servislerin var olması değil. Problem, bu karmaşık orkestrasyonun her çağıran tarafından bilinmeye başlaması.
İşte Facade Pattern burada devreye girer. Temel fikri oldukça basit:
Karmaşık alt sistemleri ortadan kaldırma. Onları tek bir kapının arkasına koy.
Facade olmadan: Controller her şeyi biliyor
Önce Facade olmadan nasıl göründüğüne bakalım:
@RestController
public class BookingController {
private final BookingCheckPipeline pipeline;
private final BookingRepository bookingRepository;
private final PaymentGateway paymentGateway;
private final TicketPdfGenerator ticketPdfGenerator;
private final BaggageSystemClient baggageSystemClient;
private final NotificationService notificationService;
private final LoyaltyPointsService loyaltyPointsService;
@PostMapping("/bookings")
public BookingResponse book(@RequestBody BookingRequest request) {
BookingContext ctx = pipeline.run(request);
PaymentResult payment = paymentGateway.charge(
request.customer().paymentMethod(), ctx.getTotalAmount());
if (!payment.isSuccessful()) {
throw new PaymentFailedException(payment.errorMessage());
}
Booking booking = bookingRepository.save(Booking.from(ctx, payment));
byte[] ticketPdf = ticketPdfGenerator.generate(booking);
baggageSystemClient.registerPassengers(booking);
notificationService.sendConfirmation(booking, ticketPdf);
loyaltyPointsService.addPoints(booking.getCustomerId(), booking.getTotalAmount());
return BookingResponse.from(booking, ticketPdf);
}
}
Bu kod çalışır. Hatta küçük bir projede bir süre gayet iyi de çalışabilir. Ama controller’ın sorumluluklarına baktığınızda bir şeylerin ters gitmeye başladığını görebilirsiniz.
Controller artık sadece HTTP ile ilgilenmiyor. Aynı zamanda:
- doğrulama pipeline’ını çağırıyor,
- ödeme alıyor,
- rezervasyonu kaydediyor,
- PDF oluşturuyor,
- bagaj sistemine haber veriyor,
- bildirim gönderiyor,
- sadakat puanı ekliyor.
Yani controller, alt sistemlerin hangi sırayla ve nasıl çağrılacağını bilmeye başlamış durumda. Bu da ciddi bir bağımlılık oluşturuyor.
Asıl problem: aynı iş akışına başka bir yerden ihtiyacımız olursa?
Diyelim ki yarın mobil uygulama için bir gRPC endpoint’i eklediniz. Veya çağrı merkezindeki operatörlerin kullanacağı bir CLI yazdınız. Rezervasyon oluşturma akışının yine aynı olması gerekiyor:
Doğrula
↓
Ödeme al
↓
Rezervasyonu kaydet
↓
Bilet oluştur
↓
Bagaj sistemine bildir
↓
Bildirim gönder
↓
Sadakat puanı ekle
Bu durumda ne yapacaksınız? Aynı 20-30 satırlık orkestrasyon kodunu başka bir yerde tekrar yazabilirsiniz. Ama bu sefer iki farklı yerde aynı business flow’u yönetmeye başlamış olursunuz. Bir yerde ödeme akışını değiştirip diğerini unutmak artık çok kolaydır.
İşte Facade tam olarak bu problemi çözer.
Facade ile: tek bir kapı
Controller’ın bütün alt sistemleri bilmesi yerine araya bir BookingFacade koyuyoruz:
graph TD
C[BookingController] --> F{{BookingFacade}}
CLI[CallCenterBookingCli] --> F
F --> P[BookingCheckPipeline]
F --> PG[PaymentGateway]
F --> R[(BookingRepository)]
F --> T[TicketPdfGenerator]
F --> B[BaggageSystemClient]
F --> N[NotificationService]
F --> L[LoyaltyPointsService]Controller artık sadece Facade’ı biliyor. Karmaşık orkestrasyonun tamamı Facade’ın arkasında kalıyor.
Farkı en net şöyle görebilirsiniz:
// Controller yedi bağımlılık taşıyor ve sırayı kendisi biliyor
BookingContext ctx = pipeline.run(request);
PaymentResult payment = paymentGateway.charge(...);
Booking booking = bookingRepository.save(...);
byte[] pdf = ticketPdfGenerator.generate(booking);
baggageSystemClient.registerPassengers(booking);
notificationService.sendConfirmation(booking, pdf);
loyaltyPointsService.addPoints(...);
// Controller tek bir bağımlılık taşıyor ve sırayı hiç bilmiyor
CompletedBooking result = bookingFacade.completeBooking(request);
1. Facade sınıfı — orkestrasyonun tek sahibi
@Service
public class BookingFacade {
private final BookingCheckPipeline pipeline;
private final BookingRepository bookingRepository;
private final PaymentGateway paymentGateway;
private final TicketPdfGenerator ticketPdfGenerator;
private final BaggageSystemClient baggageSystemClient;
private final NotificationService notificationService;
private final LoyaltyPointsService loyaltyPointsService;
public BookingFacade(BookingCheckPipeline pipeline,
BookingRepository bookingRepository,
PaymentGateway paymentGateway,
TicketPdfGenerator ticketPdfGenerator,
BaggageSystemClient baggageSystemClient,
NotificationService notificationService,
LoyaltyPointsService loyaltyPointsService) {
this.pipeline = pipeline;
this.bookingRepository = bookingRepository;
this.paymentGateway = paymentGateway;
this.ticketPdfGenerator = ticketPdfGenerator;
this.baggageSystemClient = baggageSystemClient;
this.notificationService = notificationService;
this.loyaltyPointsService = loyaltyPointsService;
}
@Transactional
public CompletedBooking completeBooking(BookingRequest request) {
// 1. Doğrulama zinciri
BookingContext ctx = pipeline.run(request);
// 2. Ödeme
PaymentResult payment = paymentGateway.charge(
request.customer().paymentMethod(), ctx.getTotalAmount());
if (!payment.isSuccessful()) {
throw new PaymentFailedException(payment.errorMessage());
}
// 3. Kayıt
Booking booking = bookingRepository.save(Booking.from(ctx, payment));
// 4. Yan sistemler — hata toleransı burada yönetilir
byte[] ticketPdf = ticketPdfGenerator.generate(booking);
baggageSystemClient.registerPassengers(booking);
notificationService.sendConfirmation(booking, ticketPdf);
loyaltyPointsService.addPoints(booking.getCustomerId(), booking.getTotalAmount());
return new CompletedBooking(booking, ticketPdf);
}
}
Burada önemli nokta şu: Facade iş mantığını yok etmiyor. İş akışının koordinasyonunu tek bir yerde topluyor.
completeBooking çağrıldığında olan şey sırayla:
Doğrulama zincirini çalıştırıyor
Uçuş var mı, koltuk müsait mi, yolcu bilgileri geçerli mi — hepsi pipeline’ın içinde kalıyor.
Ödemeyi alıyor
Başarısızsa akış burada kesiliyor. Ödeme alınmadan rezervasyon kaydedilmiyor.
Rezervasyonu kaydediyor
Artık elimizde kalıcı bir
Bookingvar.Yan sistemleri tetikliyor
Bilet, bagaj, bildirim ve sadakat puanı. Bunların sırasını da yalnızca Facade biliyor.
Örneğin ödeme alınmadan rezervasyon kaydedilmemesi gerekiyorsa, bu kuralı artık sadece BookingFacade biliyor. Böylece bu akışı kullanan diğer katmanların bunları bilmesine gerek kalmıyor.
Buradaki @Transactional göründüğü kadar masum değil
Metodun tamamı tek bir transaction içinde. Ama içinde ödeme sağlayıcısına, bagaj sistemine ve bildirim servisine yapılan ağ çağrıları var. Bu iki sorun doğurur:
- Uzun transaction. Veritabanı bağlantısı, dış servisler cevap verene kadar havuzdan tutuluyor. Yoğun anda bağlantı havuzunu bu tüketir.
- Geri alınamayan yan etkiler. Sadakat puanı eklenirken hata olursa transaction geri alınır ve rezervasyon silinir — ama müşterinin kartından çekilen para geri gelmez, gönderilen mail geri dönmez.
Transaction’ın sınırı, gerçekten geri alınabilen şeyler olmalı: yani veritabanı yazımı. Gerisi commit’ten sonra çalışmalı.
Yan etkileri transaction'ın dışına nasıl çıkarırız?
Facade’ı ikiye ayırmak en pratik yol: kısa ve gerçekten transactional olan çekirdek, sonra commit sonrası çalışan yan etkiler.
public CompletedBooking completeBooking(BookingRequest request) {
BookingContext ctx = pipeline.run(request);
PaymentResult payment = paymentGateway.charge(
request.customer().paymentMethod(), ctx.getTotalAmount());
if (!payment.isSuccessful()) {
throw new PaymentFailedException(payment.errorMessage());
}
// Yalnızca bu kısım transactional
Booking booking = bookingWriter.save(ctx, payment);
// Commit sonrası: bilet, bagaj, bildirim, sadakat
events.publishEvent(new BookingCompleted(booking.getId()));
return new CompletedBooking(booking, ticketPdfGenerator.generate(booking));
}
Dinleyici tarafında:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onBookingCompleted(BookingCompleted event) { ... }
Böylece bildirim yalnızca rezervasyon gerçekten kaydedildiyse gider. Yan sistemin geçici olarak çökmesi de rezervasyonu iptal etmez. Garantiye ihtiyacınız varsa bir adım ötesi outbox pattern’dir.
Dikkat: Facade hala ödemeyi transaction dışında alıyor. Ödeme sonrası kayıt başarısız olursa telafi (iade) gerekir — bu, dağıtık işlemlerin doğasında olan bir problem ve Facade onu çözmez, sadece nerede çözüleceğini netleştirir.
2. Controller artık sadece HTTP ile ilgileniyor
Controller’ın yeni hali oldukça basit:
@RestController
public class BookingController {
private final BookingFacade bookingFacade;
public BookingController(BookingFacade bookingFacade) {
this.bookingFacade = bookingFacade;
}
@PostMapping("/bookings")
public BookingResponse book(@RequestBody BookingRequest request) {
CompletedBooking result = bookingFacade.completeBooking(request);
return BookingResponse.from(result.booking(), result.ticketPdf());
}
}
Controller artık şunları bilmiyor:
PaymentGatewayvar mı?- Bilet nasıl oluşturuluyor?
- Bagaj sistemine ne zaman haber veriliyor?
- Bildirim nasıl gönderiliyor?
- Sadakat puanı ne zaman ekleniyor?
Controller’ın bildiği tek şey:
bookingFacade.completeBooking(request);
HTTP request’i alıyor, Facade’ı çağırıyor ve sonucu HTTP response’a çeviriyor. Bu kadar.
3. Aynı Facade’ı başka bir giriş noktasından kullanmak
Facade’ın asıl avantajı burada daha net ortaya çıkıyor. Yarın çağrı merkezi için bir CLI yazdığımızı düşünelim:
@Component
public class CallCenterBookingCli implements CommandLineRunner {
private final BookingFacade bookingFacade;
@Override
public void run(String... args) {
BookingRequest request = readFromOperatorInput();
bookingFacade.completeBooking(request);
}
}
Burada herhangi bir business logic kopyalamadık. İki giriş noktası da aynı akışı kullanıyor:
REST API
REST Controller
│
▼
BookingFacade
│
├── Validation
├── Payment
├── Booking
├── Ticket
├── Baggage
├── Notification
└── Loyalty
Çağrı merkezi
Call Center CLI
│
▼
BookingFacade
│
├── Validation
├── Payment
├── Booking
├── Ticket
├── Baggage
├── Notification
└── Loyalty
İki farklı giriş noktası, tek bir iş akışı.
Facade neyi çözüyor?
Burada Facade’ın yaptığı şeyi yanlış anlamamak önemli. Facade karmaşıklığı yok etmiyor. PaymentGateway, BaggageSystemClient, NotificationService gibi servisler hala orada.
Sadece bu karmaşıklığı tek bir kapının arkasına taşıyor. Dışarıdan bakan taraf için ise sadece:
bookingFacade.completeBooking(request);
var.
Bence Facade’ın en güzel tanımı şu:
Karmaşıklığı ortadan kaldırmaz, karmaşıklığın nerede olduğunu sınırlar.
Test etmek de kolaylaşıyor
Controller’ı test ederken artık altı farklı dependency ile uğraşmamıza gerek yok. Sadece Facade’ı mock’layabiliriz:
@Test
void basariliRezervasyondaTicketDonderir() {
when(bookingFacade.completeBooking(any()))
.thenReturn(sampleCompletedBooking());
var response = controller.book(sampleRequest());
assertThat(response.ticketNumber()).isNotNull();
}
Controller testinin amacı da zaten payment gateway’i veya bagaj sistemini test etmek değil. Controller’ın görevi:
HTTP Request
↓
Facade
↓
HTTP Response
Bu sınır artık çok daha net. Facade’ın kendisi ise ayrı test edilir. Orada ödeme, rezervasyon, ticket, bagaj ve bildirim akışının doğru sırada çalışıp çalışmadığını test edebilirsiniz.
Facade testinde verify yerine InOrder kullanmak işe yarar. Bu akışta önemli olan yalnızca adımların çağrılması değil, sırası:
InOrder inOrder = inOrder(paymentGateway, bookingRepository, notificationService);
inOrder.verify(paymentGateway).charge(any(), any());
inOrder.verify(bookingRepository).save(any());
inOrder.verify(notificationService).sendConfirmation(any(), any());
Biri bildirim gönderimini ödemenin önüne aldığında test kırılır.
Değişikliklerin etkisi azalıyor
Yarın bagaj sistemi değişti diyelim. Eski sistem:
BaggageSystemClient
yerine:
NewBaggageProvider
kullanılacak. Facade’ın içindeki dependency değişir. Controller’ın bundan haberi bile olmaz.
Aynı şekilde yeni bir notification provider’a geçtiğinizde REST endpoint’i veya çağrı merkezi CLI’ı değişmek zorunda kalmaz. Bu da Facade’ın önemli bir avantajı: değişikliklerin etkisini tek bir noktada tutmak.
Chain of Responsibility ile ilişkisi
Burada BookingCheckPipeline ile Facade arasında önemli bir ayrım var. Doğrulama zinciri klasik bir Chain of Responsibility: her halka isteğe bakar, kendi kuralını uygular ve sıradakine devreder.
Facade, pipeline’ın yerine geçmiyor. Pipeline hala kendi işini yapıyor:
BookingCheckPipeline
↓
Doğrulamalar
Facade ise daha büyük iş akışını yönetiyor:
BookingFacade
│
├── BookingCheckPipeline
├── Payment
├── Booking
├── Ticket
├── Baggage
├── Notification
└── Loyalty
Başka bir deyişle: Pipeline bir işi yapıyor, Facade ise işleri sıraya koyuyor.
| Pattern | Cevapladığı soru |
|---|---|
| Chain of Responsibility | “Bu request geçerli mi?” |
| Facade | “Bu rezervasyonu tamamlamak için hangi adımları hangi sırayla çalıştırmalıyım?” |
İki pattern farklı problemi çözüyor ve gayet güzel birlikte kullanılabiliyor.
Facade ne zaman kullanışlı?
Her servisin önüne Facade koymak gerekmiyor. Ama şu belirtileri görüyorsanız düşünmeye değer:
- Bir controller veya service çok fazla dependency almaya başladıysa.
- Aynı alt sistemleri birden fazla giriş noktası kullanıyorsa.
- Aynı orkestrasyon kodu farklı yerlerde tekrar ediyorsa.
- Bir işlemin hangi sırayla yapılması gerektiğini birden fazla sınıf biliyorsa.
- Üst katmanlar düşük seviyeli servislerin detaylarını bilmeye başladıysa.
- Bir use case giderek onlarca servis çağrısına dönüşüyorsa.
Bunlar genellikle “burada bir Facade olabilir” sinyalidir.
Facade'ın kendi tuzağı
Facade tek bir use case’i koordine ettiği sürece işini yapar. Rezervasyon oluşturma, iptal, değişiklik, check-in ve iade işlemlerinin hepsini aynı sınıfa doldurduğunuzda ise elinizde bir “god object” kalır — kaçtığınız şeyin daha büyüğü.
Ölçüt basit: bir Facade, bir iş akışı. BookingFacade yerine CreateBookingFacade, CancelBookingFacade, CheckInFacade gibi ayırmak neredeyse her zaman daha sağlıklıdır.
Gerçek hayatta nerelerde görürüz?
Facade Pattern aslında düşündüğünüzden çok daha yaygın.
Örneğin ödeme sistemlerinde, tek bir pay() veya charge() çağrısının arkasında authentication, HTTP request, retry, serialization, error handling ve response mapping gibi onlarca detay olabilir.
AWS SDK’ları da benzer bir fikir sunar. Siz yüksek seviyeli bir API çağırırken arka plandaki düşük seviyeli HTTP ve protokol detaylarıyla uğraşmazsınız.
Mikroservis tarafında BFF (Backend for Frontend) katmanı da benzer bir role sahip olabilir. Bir frontend’in ihtiyaç duyduğu veriyi toplamak için birden fazla servise çağrı yapar ve frontend’e bunların üzerinde daha basit bir API sunar.
BFF birebir Facade değildir; ancak aynı temel fikri taşır:
Karmaşık bir sistemi dışarıya daha basit bir arayüzle sunmak.
Altta yatan kural
Facade Pattern için akılda tutulması gereken en basit kural şu:
Bir sınıfın arkasında çok fazla sistem varsa, dışarıya hepsini göstermek zorunda değilsiniz.
Controller’ın:
PaymentGateway
BookingRepository
TicketPdfGenerator
BaggageSystemClient
NotificationService
LoyaltyPointsService
gibi altı farklı sistemi bilmesine gerek yok. Bunun yerine:
Controller
↓
BookingFacade
↓
Karmaşık alt sistemler
diyebilirsiniz.
Facade, karmaşıklığı sihirli bir şekilde ortadan kaldırmaz. Sadece karmaşıklığın tek bir yerde yaşamasını sağlar.
Ve çoğu zaman iyi tasarımın amacı da tam olarak budur: sistemi basitleştirmek değil, karmaşıklığın sınırlarını netleştirmek.

Yorumlar