Dosya Tabanlı Entegrasyon: SFTP ve FTP ile Dosya Alışverişi, Klasör İzleme, Idempotent İşleme ve Reconciliation
Dosya tabanlı entegrasyonda dosyayı ancak karşı taraf hazır olduğunu işaretlediğinde (.done ya da geçici adla yükleyip rename) okursun, host key'i doğrularsın ve işlenen dosyaları içeriğin hash'iyle bir ledger'a yazarsın; böylece yarım ya da iki kez işleme olmaz. Bookstore'da tedarikçinin SFTP'deki stok dosyasını SSH.NET ile, bankanın FTPS'teki tahsilat dosyasını FluentFTP ile alıp siparişlerle mutabakat yapıyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10, SSH.NET 2026.0.1 ve FluentFTP 55.1 ile derlenip denenmiştir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) her gece iki dosya alıyor: tedarikçi yayınevinin SFTP (SSH File Transfer Protocol; şifreli SSH bağlantısı üzerinden dosya aktarımı) sunucusuna bıraktığı stok ve fiyat dosyası, bir de bankanın kurumsal müşterilerden gelen havaleleri listelediği tahsilat dosyası. Üç soru:
Birincisi: tedarikçi dosyayı yazmayı bitirmeden senin job'ın onu okursa ne olur? Exception alır mısın?
İkincisi: tedarikçi dünkü dosyayı yanlışlıkla yeni bir adla tekrar gönderiyor. Dosya adına göre "işlendi" kaydı tutuyorsan stoklara ne olur?
Üçüncüsü: banka sunucusu TLS (Transport Layer Security; bağlantıyı şifreleyen protokol) zorunlu tutuyor ve sen düz FTP (File Transfer Protocol; klasik dosya aktarım protokolü) ile bağlanıyorsun. Ne alırsın?
Birincinin cevabı "çoğu zaman exception almazsın": yarım satır bazen geçersiz olur ve reddedilir, ama 9780441013593;479,00;TRY;2 gibi bir yerde kesilirse geçerli görünür ve stok 20 yerine 2 olur. İkincinin cevabı "dün uygulanan değerler, aradaki satışları ezerek bir daha uygulanır". Üçüncüsü aşağıdaki gerçek çıktıda: 550 SSL/TLS required on the control channel. Bu serinin son yazısında HTTP'nin hiç olmadığı bir entegrasyona bakıyoruz: dosya alışverişi.
Dosya Tabanlı Entegrasyonun Kuralları
HTTP API'de sözleşme endpoint'ler ve şemalardır. Dosya alışverişinde sözleşme klasörler, dosya adları ve "dosya hazır" sinyalidir; bunlar konuşulmadan başlanan entegrasyon ilk ay içinde bir dosyayı yarım ya da iki kez işler. Bookstore'un tedarikçiyle anlaştığı kurallar:
- Tedarikçi her gece
outbound/klasörünestock-prices-YYYYMMDD.csvbırakır; dosya tamamlanınca yanına boş bir.donedosyası yazar. - Bookstore işlediği dosyayı
outbound/processed/klasörüne taşır; hatalı satırlarıinbound/klasörüne bir rapor dosyası olarak geri koyar. - Format CSV (değerleri noktalı virgülle ayrılmış düz metin tablo),
UTF-8, başlıkisbn;list_price;currency;stock, fiyatlar Excel'den çıktığı için ondalık virgüllü. - Fiyat ve stok mutlak değerdir ("stok 20"), değişim değil ("stok +3").
Son madde küçük görünüyor ama idempotency'nin yarısı bu. Bir dosya iki kez uygulanırsa mutlak değerler aynı sonucu verir; değişimler stoğu iki kez artırır. Türkçe Excel'in noktalı virgül ve ondalık virgül alışkanlıkları .NET'te I/O İşlemleri serisinin CSV yazısında; toplu import'ta satır bazlı hata raporu da Çözüm Desenleri serisinin Toplu Import yazısında. Bir dosyanın Bookstore'daki yolculuğu:

SSH.NET ile SFTP: Key ile Giriş ve Host Key Doğrulaması
Gerçek tedarikçi yerine repo'da docker/fake-sftp var: OpenSSH çalıştıran, yalnız key ile girişe izin veren ve supplier kullanıcısını kendi klasörüne kapatan küçük bir imaj. Önce test key'lerini repo'nun .local/sftp klasöründe üretiyoruz (bu klasör .gitignore'da; parola sorduğunda boş geçebilirsin, bunlar yalnız test key'leri):
ssh-keygen -t ed25519 -f .local/sftp/ssh_host_ed25519_key -C fake-supplier-host
ssh-keygen -t rsa -b 3072 -f .local/sftp/ssh_host_rsa_key -C fake-supplier-host
ssh-keygen -t ed25519 -f .local/sftp/bookstore_supplier -C bookstore-test
docker build -t resseries-sftp:dev docker/fake-sftp
docker run -d --name resseries-sftp -p 127.0.0.1:52422:22 -v "$PWD/.local/sftp:/keys:ro" resseries-sftp:dev
İlk iki key sunucunun kimliği, üçüncüsü Bookstore'un. SSH (Secure Shell; şifreli uzak bağlantı protokolü) bağlantısında sunucu önce kendi host key'ini gösterir; client bu key'i tanımıyorsa karşısındaki sunucunun gerçekten tedarikçi olduğunu bilemez. ssh komutunun ilk bağlantıda sorduğu "Are you sure you want to continue connecting?" sorusu budur. Kodda karşılığı HostKeyReceived event'i:
// Host key kontrolü: bu olmadan araya giren herkes tedarikçinin sunucusu gibi davranabilir
client.HostKeyReceived += (_, e) =>
{
e.CanTrust = string.Equals("SHA256:" + e.FingerPrintSHA256, settings.HostKeyFingerprint, StringComparison.Ordinal);
if (!e.CanTrust)
{
logger.LogError("Host key {Algorithm} SHA256:{Fingerprint} of {Drop} does not match the configured fingerprint",
e.HostKeyName, e.FingerPrintSHA256, name);
}
};
Beklenen fingerprint configuration'dan geliyor. Değeri tedarikçiden e-posta dışında bir kanalla teyit etmelisin; kendi test sunucunda ise iki yoldan okuyabilirsin:
ssh-keyscan -p 52422 -t ed25519 127.0.0.1 | ssh-keygen -lf -
ssh-keygen -lf .local/sftp/ssh_host_ed25519_key.pub
256 SHA256:GxPPYJtPnULoH0Lxu0qpPdBjvgmRXPHcVhBFJrT1S1Y [127.0.0.1]:52422 (ED25519)
256 SHA256:GxPPYJtPnULoH0Lxu0qpPdBjvgmRXPHcVhBFJrT1S1Y fake-supplier-host (ED25519)
appsettings.json'da bu alan bilerek SHA256:set-this-to-your-test-host-key diye duruyor; kendi değerini girmeden uygulama bağlanmıyor. Önce yanlış değerle, sunucunun RSA (yaygın bir açık anahtar algoritması) key'inin fingerprint'iyle denedim:
fail: Bookstore.FileDrops.SftpFileDrop[0]
Host key ssh-ed25519 SHA256:GxPPYJtPnULoH0Lxu0qpPdBjvgmRXPHcVhBFJrT1S1Y of supplier does not match the configured fingerprint
warn: Bookstore.Infrastructure.ExternalServiceExceptionHandler[0]
External service failure mapped to 503
Bookstore.FileDrops.FileDropException: Could not connect to the 'supplier' SFTP drop.
---> Renci.SshNet.Common.SshConnectionException: Host key could not be verified.
SSH.NET bu sunucuyla ed25519 key'ini seçti. Sunucu birden çok host key sunuyorsa hangisinin seçileceği iki tarafın algoritma listelerine bağlı; tedarikçiden aldığın fingerprint'in hangi algoritmaya ait olduğunu da not et. Kimlik doğrulama için private key PrivateKeyFile ile yükleniyor ve parola hiç yok. Production'da bu dosya da bir secret store'dan gelmeli.
Yarım Dosyayı Okumamak: .done İşareti ve Geçici Adla Yükleme
Job dosyayı listede gördüğünde dosyanın tamamlandığını bilemez. Boyutun bir süre değişmemesini beklemek gibi tahmine dayalı yöntemler yerine sözleşmedeki işarete bakıyoruz:
var markers = files.Where(f => f.Name.EndsWith(".done", StringComparison.Ordinal)).Select(f => f.Name[..^5]).ToHashSet();
var results = new List<FeedFileResult>();
foreach (var file in files.Where(f => f.Name.EndsWith(".csv", StringComparison.Ordinal)).OrderBy(f => f.Name))
{
if (!markers.Contains(file.Name))
{
// Hâlâ yazılıyor (ya da tedarikçi yarıda kaldı): işareti olmayan dosyayı asla okuma
logger.LogInformation("Skipping {File}: no .done marker yet", file.Name);
results.Add(new FeedFileResult(file.Name, FeedFileStatus.WaitingForMarker));
continue;
}
results.Add(await ProcessAsync(session, file.Name, cancellationToken));
}
Ters yönde, yani dosyayı biz yazarken aynı kuralı karşı tarafa uyguluyoruz. Hatalı satır raporu önce .part uzantısıyla yükleniyor, bitince yeniden adlandırılıyor. Rename sunucu tarafında tek bir işlem; karşı taraf ya hiç dosya görmüyor ya da tamamını görüyor:
public async Task UploadAsync(string path, Stream content, CancellationToken cancellationToken)
{
var temporary = path + ".part";
await client.UploadFileAsync(content, temporary, cancellationToken);
// SFTP rename hedef varsa başarısız olur, bu yüzden önceki sürümü önce sil
if (await client.ExistsAsync(path, cancellationToken))
{
await client.DeleteFileAsync(path, cancellationToken);
}
await client.RenameFileAsync(temporary, path, cancellationToken);
}
Tedarikçinin outbound/ klasörüne iki dosya koydum: stock-prices-20261114.csv ve .done işareti, bir de yazılması bitmemiş stock-prices-20261115.csv. Back office'teki "şimdi çalıştır" endpoint'ine POST /admin/file-drops/supplier-feed/run isteği gönderince:
[{"name":"stock-prices-20261114.csv","status":"Processed","rows":5,"applied":3,"failed":2,"errorReport":"stock-prices-20261114.errors.csv"},{"name":"stock-prices-20261115.csv","status":"WaitingForMarker","rows":0,"applied":0,"failed":0,"errorReport":null}]
Beş satırdan üçü uygulandı. Sunucuda kalan dosyalar ve tedarikçiye giden rapor:
./inbound/stock-prices-20261114.errors.csv
./outbound/processed/stock-prices-20261114.csv
./outbound/processed/stock-prices-20261114.csv.done
./outbound/stock-prices-20261115.csv
line;isbn;error
5;9780141439549;unsupported currency
6;9780000000001;invalid ISBN-13
Satır numarası, ISBN (kitapların uluslararası standart numarası) ve neden: tedarikçinin düzeltmek için ihtiyacı olan her şey. Raporun içinde stack trace ya da iç sistem ayrıntısı yok.
Polling Job ve FileSystemWatcher'ın Sınırları
Dosyalar başka bir sunucuda olduğu için "dosya geldi" event'i yok; job belirli aralıklarla bakıyor:
using var timer = new PeriodicTimer(options.Value.Interval, time);
do
{
using var scope = scopes.CreateScope();
try
{
await scope.ServiceProvider.GetRequiredService<SupplierFeedProcessor>().RunAsync(stoppingToken);
await scope.ServiceProvider.GetRequiredService<BankStatementReconciler>().RunAsync(stoppingToken);
}
catch (FileDropException ex)
{
// Sonraki tur retry'ın kendisi; tekrar ederse alarm çalar (bkz. "Üretimde Dikkat")
logger.LogWarning(ex, "File drop run failed, trying again in {Interval}", options.Value.Interval);
}
}
while (await timer.WaitForNextTickAsync(stoppingToken));
Polling'in iyi yanı, kaçırılan bir turun bir sonraki turda kendiliğinden telafi edilmesi; bağlantı hatasında ayrı bir retry pipeline'ına gerek kalmıyor. Interval'ı 10 saniyeye indirip 24 saniye çalıştırınca yarım dosya üç turda da bekletildi:
info: Bookstore.Inventory.SupplierFeed.SupplierFeedProcessor[0]
Skipping stock-prices-20261115.csv: no .done marker yet
info: Bookstore.Inventory.SupplierFeed.SupplierFeedProcessor[0]
Skipping stock-prices-20261115.csv: no .done marker yet
info: Bookstore.Inventory.SupplierFeed.SupplierFeedProcessor[0]
Skipping stock-prices-20261115.csv: no .done marker yet
Admin endpoint'i ile job'ın aynı dosyayı aynı anda işlememesi için processor'da bir SemaphoreSlim var. API birden çok instance'la çalışıyorsa bu yetmez; distributed lock ya da Hangfire'ın tek instance'ta çalışan recurring job'ları gerekir (Background Job'lar serisinin Hangfire yazısı).
Dosyalar kendi sunucundaki bir klasöre geliyorsa FileSystemWatcher cazip görünür, ama polling'in yerini tutmaz. Değişiklikler işletim sisteminin doldurduğu bir buffer'da birikiyor; kısa sürede çok değişiklik olursa buffer taşıyor, olaylar kayboluyor ve yalnız Error event'i geliyor. Network üzerinden izlenen klasörde InternalBufferSize en fazla 64 KB olabiliyor. Bir dosyayı taşımak gibi tek bir işlem birden çok Changed, Created ve Deleted olayı üretebiliyor; antivirüs gibi başka uygulamalar da ek olay doğuruyor. Ve Created olayı yazmanın bittiğini değil, dosyanın oluştuğunu söylüyor. Watcher'ı kullansan bile onu yalnız "şimdi bir tur çalıştır" tetikleyicisi olarak kullan, işin kendisini yine periyodik taramaya bırak. Bu bölümde resmî belgelerden yararlanılmıştır.
Aynı Dosyayı İki Kez İşlememek: Hash, Ledger ve Mutlak Değerler
Dosya adı kimlik değil: tedarikçi aynı içeriği yeni bir adla gönderebilir, ya da farklı içeriği aynı adla. Kimlik olarak içeriğin SHA256 (içeriğin 256 bitlik özetini üreten hash algoritması) değerini kullanıyoruz ve işlenen dosyaları kalıcı bir ledger'a yazıyoruz:
var hash = Convert.ToHexStringLower(SHA256.HashData(await File.ReadAllBytesAsync(incoming, cancellationToken)));
// 2. Aynı içerik daha önce görüldü (yeniden gönderim ya da işlemeden sonra çökme): tekrar uygulama
if (ledger.Find(Scope, hash) is { } earlier)
{
logger.LogInformation("{File} has the same content as {Earlier}, already processed at {At}", name, earlier.Detail, earlier.At);
File.Delete(incoming);
await ArchiveRemoteAsync(session, name, cancellationToken);
return new FeedFileResult(name, FeedFileStatus.AlreadyProcessed);
}
Tedarikçi dünkü dosyayı stock-prices-20261114-resend.csv adıyla tekrar gönderdiğinde:
[{"name":"stock-prices-20261114-resend.csv","status":"AlreadyProcessed","rows":0,"applied":0,"failed":0,"errorReport":null},{"name":"stock-prices-20261115.csv","status":"WaitingForMarker","rows":0,"applied":0,"failed":0,"errorReport":null}]
stock-prices-20261114-resend.csv has the same content as stock-prices-20261114.csv, already processed at 11/15/2026 08:37:02 +00:00
Sıralama da önemli. Uygulamadan sonra önce ledger'a yazıyor, sonra dosyayı arşivliyoruz:
// 5. Önce kaydet, sonra arşivle: arada çökersek sonraki tur hash'i bulur ve yalnız arşivler
ledger.Add(Scope, hash, name, time.GetUtcNow());
Ledger'a yazmadan önce çökersek dosya bir sonraki turda yeniden işleniyor; değerler mutlak olduğu için sonuç aynı. Yazdıktan sonra çökersek hash bulunuyor ve dosya yalnız arşivleniyor. Ledger bu örnekte bir JSON-lines dosyası. Bookstore'da (Scope, Key) üzerinde unique index'i olan bir tablo; testlerden biri ledger'ı yeniden açarak uygulamanın yeniden başlatılmasını taklit ediyor. Milyonlarca satırlık dosyalarda "nerede kalmıştım" sorusu da gelir; chunk ve checkpoint ile kaldığı yerden devam etmek Background Job'lar serisinin uzun süren ve batch job'lar yazısında.
FluentFTP ile FTPS: Bankanın Tahsilat Dosyası
Banka dosyayı SFTP yerine FTPS (TLS ile şifrelenmiş FTP) sunucusunda veriyor. Repo'daki tools/fake-bank-ftps.py, pyftpdlib ile 127.0.0.1:52421'de TLS zorunlu bir FTP sunucusu açıyor ve güvenilecek sertifikayı .local/bank-ftp/cert.pem dosyasına yazıyor. pyftpdlib ve pyOpenSSL paketleri kurulu bir Python ile repo kökünden başlatılıyor:
python tools/fake-bank-ftps.py
Client tarafı FluentFTP'nin AsyncFtpClient'ı:
var client = new AsyncFtpClient(settings.Host, settings.UserName, settings.Password, settings.Port);
client.Config.ConnectTimeout = (int)settings.Timeout.TotalMilliseconds;
client.Config.ReadTimeout = (int)settings.Timeout.TotalMilliseconds;
client.Config.DataConnectionType = FtpDataConnectionType.PASV;
if (settings.UseTls)
{
client.Config.EncryptionMode = FtpEncryptionMode.Explicit;
client.Config.DataConnectionEncryption = true;
if (settings.TrustedCertificatePath is { Length: > 0 } path)
{
var trusted = X509CertificateLoader.LoadCertificateFromFile(path);
client.ValidateCertificate += (_, e) => e.Accept = e.Certificate.GetRawCertData().AsSpan().SequenceEqual(trusted.RawData);
}
}
FluentFTP'nin AsyncFtpClient method'larında Async eki yok: Connect, GetListing, DownloadStream, MoveFile hepsi Task dönüyor ve CancellationToken alıyor. İki yanlış ayarla denedim. Sabitlenmiş sertifika olmadan, yani makinenin güven deposuyla:
Bookstore.FileDrops.FileDropException: Could not connect to the 'bank' FTP drop.
---> System.Security.Authentication.AuthenticationException: The remote certificate was rejected by the provided RemoteCertificateValidationCallback.
UseTls kapalıyken, yani düz FTP ile:
Bookstore.FileDrops.FileDropException: Could not connect to the 'bank' FTP drop.
---> FluentFTP.Exceptions.FtpAuthenticationException: Code: 550 Message: SSL/TLS required on the control channel.
Düz FTP'de kullanıcı adı ve parola ağda açık metin gidiyor. Karşı taraf yalnız düz FTP sunuyorsa bunu bir risk olarak yazılı hâle getir ve bağlantıyı en azından VPN (iki ağ arasında şifreli tünel) gibi şifreli bir kanalın içine al.
Reconciliation: Ekstreyi Siparişlerle Eşleştirmek
Kurumsal müşteriler havaleyle ödüyor ve açıklamaya sipariş referansını kendileri yazıyor: BKS-1003, bks 1004, bazen hiçbir şey. 06. yazıda ödeme sağlayıcısının kayıtlarıyla yaptığımız reconciliation'ın dosya hâli bu:
// Müşteriler referansı elle yazıyor: "BKS-1003", "bks 1003", "BKS1003 school payment"
[GeneratedRegex(@"\bBKS[-\s]?(\d{4,})\b", RegexOptions.IgnoreCase)]
private static partial Regex OrderReference();
Bu sefer idempotency dosya başına değil, banka işlem numarası başına. Bankalar ekstreyi yeniden gönderebiliyor ve bir günün dosyası önceki günlerin satırlarını tekrar içerebiliyor:
// Dosya başına değil işlem başına idempotency: bankalar ekstreyi yeniden gönderir, bir günün dosyası önceki satırları tekrar içerebilir
private ReconciliationLine Match(StatementLine line)
{
if (ledger.Find(Scope, line.TransactionId) is { } seen)
{
return new(line.TransactionId, line.Amount, line.Description, MatchOutcome.AlreadySeen, null, $"seen at {seen.At:O}");
}
Üç satırlık ilk ekstre:
[{"transactionId":"TX-20261114-0001","amount":4131.00,"description":"BKS-1003 School 0042 book order","outcome":"Matched","orderId":1003,"note":null},{"transactionId":"TX-20261114-0002","amount":2000.00,"description":"bks 1004 library payment","outcome":"AmountMismatch","orderId":1004,"note":"expected 2250.00 TRY"},{"transactionId":"TX-20261114-0003","amount":980.00,"description":"Payment for books","outcome":"UnknownReference","orderId":null,"note":"no order reference found"}]
1003 numaralı sipariş Paid oldu. Diğer iki satır bir hata değil, bir iş: retry ile çözülmüyor, back office'te bir kişinin bakması gerekiyor ve job bunları Warning olarak logluyor. Ertesi gün banka ilk satırı yeni dosyada tekrar gönderdiğinde:
[{"transactionId":"TX-20261114-0001","amount":4131.00,"description":"BKS-1003 School 0042 book order","outcome":"AlreadySeen","orderId":null,"note":"seen at 2026-11-15T08:37:10.6939375+00:00"}]
İşlemciler SFTP'ye ya da FTPS'e değil IFileDrop port'una bağlı. Testler bu yüzden sunucu gerektirmiyor; LocalFolderFileDrop geçici bir klasörde aynı akışı çalıştırıyor:
Test run summary: Passed!
total: 23
failed: 0
succeeded: 23
skipped: 0
duration: 17s 478ms
Üretimde Dikkat
Sessizliği izle. Dosya tabanlı entegrasyonun en sık arızası hata değil, dosyanın hiç gelmemesi. "Saat 06:00'da bugünün dosyası işlenmemişse alarm" kuralı, bağlantı hatası alarmından daha değerli. Aynı şekilde WaitingForMarker durumunda saatlerce bekleyen bir dosya da tedarikçi tarafında yarım kalmış bir işin işareti.
Secret'ları koddan ve repodan uzak tut. SFTP private key'i, FTPS parolası ve bu yazıdaki fingerprint dahil tüm bağlantı bilgileri Configuration ve Secret Management serisinde anlattığımız gibi bir secret store'dan gelmeli; appsettings.json'daki değerler yalnız lokal fake'ler için.
Yerel klasörleri yönet. processed/ klasörü sonsuza kadar büyür; saklama süresini muhasebe ve denetim ihtiyacına göre belirle ve eski dosyaları arşivle. Banka ekstreleri kişisel ve finansal veri içerir; klasör izinlerini buna göre ver.
Sık Düşülen Tuzaklar
Host key doğrulamasını kapatmak. e.CanTrust = true ilk gün işi çözer, ama araya giren biri sahte bir stok dosyası verebilir ya da senin yüklediğin dosyaları okuyabilir.
Yazılması bitmemiş dosyayı okumak. .done işareti ya da geçici adla yükleyip rename etmek konusunda karşı tarafla anlaş; dosya boyutuna ya da saate bakarak tahmin yürütme.
Aynı dosyayı iki kez işlemek. Kimlik için dosya adını değil içeriğin hash'ini kullan, değerleri mümkünse mutlak tut, "işlendi" kaydını arşivlemeden önce yaz.
Parolayı kodda tutmak. Özellikle FTP entegrasyonlarında kullanıcı adı ve parolanın bir sabitte durduğu çok görülüyor; bu bilgiler commit geçmişinde yıllarca kalır.
Küçük Bir Deneme
★☆☆ stock-prices-20261115.csv için .done işaretini oluştur ve endpoint'i tekrar çağır. Yarım satır hangi hatayla reddediliyor?
★★☆ Banka dosyasında aynı siparişe iki ayrı havale geldiğini düşün (2000,00 ve 250,00 TL). Compare'i kısmi ödemeleri toplayacak şekilde değiştir ve bir test yaz. Fazla ödemede ne yapmalı?
★★★ Ledger'ı SQL Server'da (Scope, Key) unique index'li bir tabloya taşı ve iki API instance'ının aynı dosyayı aynı anda işlemeye çalıştığı bir test yaz. Hangisinin kazandığını ne belirliyor?
Bu yazıyla seri bitiyor. Bookstore'un dış dünyayla konuştuğu her kanal artık aynı soruların cevabını veriyor: ne kadar bekliyoruz, neyi tekrar deniyoruz, aynı işi iki kez yapmamayı nasıl garanti ediyoruz ve sonucu nasıl doğruluyoruz. HTTP'de timeout ve Idempotency-Key, webhook'ta imza ve inbox, önceki yazıdaki SOAP (XML tabanlı web servis protokolü) entegrasyonunda channel abort'u ve faturanın sabit kimliği, dosyada .done işareti, hash ve ledger. Kullanıcının isteği içinde hiç dış çağrı yapmamanın altyapısını Message Queue'larla Çalışmak serisinde, akışın tasarımını Messaging ve Event-Driven Architecture serisinde bulabilirsin.