Sequence Diagram 2: alt, opt, loop, par, break ve ref ile Hata Yolları ve Asenkron Akışlar
Combined fragment'ler sequence diagram'a koşul (alt, opt), erken çıkış (break), tekrar (loop), paralellik (par) ve bölünmezlik (critical) ekler, ref ise başka bir diyagramı çağırır; asenkron mesaj açık ok ucuyla çizilir ve await edilse bile gönderen sonucu beklemiyorsa asenkrondur. Yazıda İK Portalı'nın onay akışı, yetersiz bakiye hatası ve queue üzerinden giden e-posta ile takvim kaydı gerçek koddan çiziliyor.
Sürüm notu: Bu yazıdaki diyagramlar UML (Unified Modeling Language) 2.5.1 notasyonuyla çizilmiştir; C# örnekleri .NET 10 ve Entity Framework Core (EF Core) 10 ile, diagrams-as-code örnekleri PlantUML 1.2026.9 ve Mermaid 12.3.0 ile denenmiştir.
Önce Tahmin Et: İki Talep, Sekiz Gün
İK (insan kaynakları) Portalı'nda Ada'nın 2026 yıllık izin bakiyesinde 8 gün var. Ada aynı hafta iki ayrı yıllık izin talebi gönderiyor: #42 (21–25 Aralık, 5 iş günü) ve #44 (30 Kasım – 4 Aralık, 5 iş günü). Gönderim anında iki talep de bakiye kontrolünden geçiyor, çünkü beklemedeki talepler bakiyeden düşülmüyor:
{"id":42,"status":"PendingManagerApproval","workingDays":5}
{"id":43,"status":"PendingManagerApproval","workingDays":2}
{"id":44,"status":"PendingManagerApproval","workingDays":5}
(#43, iki günlük bir mazeret izni; bakiyeyi etkilemiyor.) Sonra yönetici Alan #42'yi onaylıyor, talep beş gün sürdüğü için direktör Grace'e gidiyor ve Grace de onaylıyor. Onay kodu şu:
public async Task<LeaveRequest> ApproveAsync(int requestId, int approverId, string? comment, CancellationToken ct)
{
LeaveRequest request = await requests.FindAsync(requestId, ct)
?? throw new LeaveRuleException($"Leave request {requestId} not found.");
Employee approver = await requests.FindEmployeeAsync(approverId, ct)
?? throw new LeaveRuleException($"Employee {approverId} not found.");
LeaveType type = request.LeaveType;
int year = request.Period.Start.Year;
// R8: onay anında bakiye yeniden kontrol edilir
if (type.DeductsFromBalance &&
!await balances.HasEnoughBalanceAsync(request.EmployeeId, type, year, request.WorkingDays, ct))
throw new LeaveRuleException("Balance is no longer sufficient.");
ApprovalStep step = request.Status == LeaveStatus.PendingDirectorApproval
? ApprovalStep.Director
: ApprovalStep.Manager;
request.Approve(approver, comment, clock.GetUtcNow());
if (request.Status == LeaveStatus.Approved && type.DeductsFromBalance)
await balances.DeductAsync(request.EmployeeId, type, year, request.WorkingDays, ct);
await requests.SaveChangesAsync(ct);
await events.PublishAsync(new LeaveRequestApproved(request.Id, step, request.Status), ct);
return request;
}
İki soru. Alan şimdi #44'ü onaylamaya çalışınca ne olur? Ve Ada'ya "talebin direktör onayında" e-postası, Grace'in kararından önce mi sonra mı ulaşır?
Cevap: Bir 409 ve Geç Gelen Bir E-posta
Gerçek koşu, okunsun diye kısaltılmış hâliyle. HTTP (Hypertext Transfer Protocol) tarafında:
POST /leave-requests/42/approve {"approverId":3} -> {"id":42,"status":"PendingDirectorApproval"}
POST /leave-requests/42/approve {"approverId":1} -> {"id":42,"status":"Approved"}
POST /leave-requests/44/approve {"approverId":3} -> 409 {"title":"Conflict","detail":"Balance is no longer sufficient."}
#42 onaylanınca UsedDays 6'dan 11'e çıktı, kalan 3 gün #44'ün 5 gününe yetmiyor; R8 kuralı devreye girdi. E-posta tarafında ise e-posta sunucusunun ilk denemede cevap vermediği bir koşu (log kategorilerindeki namespace'leri kısalttım):
23:21:44.702 Alan approved #42 -> PendingDirectorApproval
23:21:44.733 Grace approved #42 -> Approved
23:21:46.805 warn: LeaveApprovedConsumer[0] Mail attempt 1 timed out after 00:00:02
23:21:46.806 info: FlakyMailSender[0] Mail to ada@hrportal.test: Leave request #42: PendingDirectorApproval
23:21:46.806 info: FlakyMailSender[0] Mail to grace@hrportal.test: Leave request #42 waits for your approval
23:21:46.810 info: LoggingCalendarGateway[0] Calendar: add leave 42 2026-12-21..2026-12-25
23:21:46.810 info: FlakyMailSender[0] Mail to ada@hrportal.test: Leave request #42: Approved
Grace'in "onayını bekleyen bir talep var" e-postası, Grace talebi onayladıktan iki saniye sonra gitti. Demo'da iki onay arasında 31 milisaniye var, gerçek hayatta saatler olur, ama sıralama sorunu aynı: onay senkron, bildirim asenkron. Önceki yazıdaki sequence diagram'ın bu iki davranışı anlatamamasının sebebi, tek bir mutlu yol çizmesiydi. Koşulları, tekrarları, paralelliği ve asenkron mesajları çizmenin aracı combined fragment'ler.
Combined Fragment Nedir?
Combined fragment, sequence diagram'ın bir bölgesini çerçeveleyip ona bir interaction operator uygulayan yapı. Çerçevenin sol üst köşesindeki beşgen etikette operator yazar (alt, loop...), çerçeve kesikli çizgilerle operand'lara bölünebilir ve her operand'ın başında köşeli parantez içinde bir guard (koşul) olabilir: [enough == false], [else]. UML 2.5.1 on iki operator tanımlıyor: seq, alt, opt, break, par, strict, loop, critical, neg, assert, ignore, consider. Günlük işte kullandıkların ve koddaki karşılıkları:
| Operator | Anlamı | C# karşılığı |
|---|---|---|
alt | Guard'ı doğru olan tek operand çalışır | if / else, switch |
opt | Tek operand; guard doğruysa çalışır | else'siz if |
break | Guard doğruysa operand çalışır, çevreleyen akışın kalanı atlanır | throw, erken return |
loop | Operand en az min, en çok max kez tekrarlanır | for, while, retry döngüsü |
par | Operand'lar birbirinden bağımsız, iç içe geçebilir | Task.WhenAll |
critical | Bölge araya başka bir şey girmeden yürür | lock, transaction, concurrency token |
ref (interaction use) | Başka bir interaction'ı buraya çağırır | ayrı belgelenmiş bir method ya da akış |
ref teknik olarak combined fragment değil, interaction use; ama aynı çerçeve notasyonunu kullandığı için birlikte anılır. neg (bu dizi geçersizdir) ve assert (yalnız bu dizi geçerlidir) daha çok test senaryosu belgelerken işe yarar.
alt, opt ve break: Onay Akışındaki Koşullar

Şekil ApproveAsync'in sequence diagram'ı. Yukarıdan aşağı okuyalım.
break [enough == false]: bakiye yetmiyorsa çerçevenin içindeki mesajlar çalışır (LeaveRuleException, sonra 409 Conflict) ve çevreleyen interaction'ın geri kalanı atlanır. Koddaki throw tam olarak bu. break ile alt'ın farkı, break'in akışı orada bitirmesi: alt kullansaydın "else" kolunu da çizip ikisinin sonra aynı noktada birleştiğini söylemiş olurdun. Çerçevenin SPA'dan Service'e kadar üç lifeline'ı kapsaması da bilinçli: break fragment'i, kesintiden etkilenen bütün lifeline'ları kapsamalı.
alt [step = Manager ∧ RequiresDirectorApproval] / [else]: iki operand'lı bir seçim. Birinci operand'da talep PendingDirectorApproval'a geçiyor, ikincisinde Approved'a geçiyor ve bakiye düşülüyor. Lifeline üzerindeki yuvarlak köşeli kutular state invariant: "bu noktada req42 bu durumda olmalı" diyen bir koşul. Durum geçişlerinin tamamını göstermek state machine diagram'ın işi (serinin State Machine Diagram yazısı); sequence diagram'da yalnız senaryo için önemli olan durumları işaretlemek yeterli.
Koddaki karşılık iki parçaya bölünmüş durumda: durum seçimi LeaveRequest.Approve'un içinde, bakiyeden düşme ise service'teki if'te. Diyagram bu ikisini tek bir alt'ta birleştirince "direktör onayına giden talepte bakiye düşülmüyor" kuralı tek bakışta okunuyor. opt'u sonraki şekilde göreceğiz: else'i olmayan, tek operand'lı alt.
ref: Başka Bir Diyagrama Atıf
Şekildeki ref çerçevesi "Talebi ve onaycıyı yükle" adlı başka bir interaction'ı çağırıyor: FindAsync ve FindEmployeeAsync ile iki SELECT. Onu ayrı çizdik (önceki yazıdaki diyagrama benziyor) ve burada yalnız adıyla andık. Interaction use'un kodda en yakın karşılığı method çağrısı: içini bilmeden adına güvendiğin bir adım. Çerçeve, kapsadığı lifeline'ların (Service ve Repository) bu alt akışa katıldığını söylüyor.
Büyük senaryoları okunur tutmanın en etkili yolu bu. Uçtan uca bir "izin talebi yaşam döngüsü" diyagramı, "Oluştur", "Yönetici onayı", "Direktör onayı" ve "İptal" adlı ref çerçevelerinden oluşan yarım sayfalık bir özet olabilir; her biri kendi diyagramına gider. Bu fikrin tam diyagram hâli, interaction overview diagram, serinin sonraki yazılarında activity diagram'la birlikte anılacak.
loop, par ve critical
loop'un söz dizimi loop(min, max) [guard]: operand en az min kez çalışır, sonra guard doğru olduğu sürece en fazla max kez. Retry için loop(1,3) ve "gönderilemedi" guard'ı tam karşılık: en az bir deneme, en fazla üç.
par, operand'ların birbirinden bağımsız ilerlediğini ve mesajlarının herhangi bir sırayla iç içe geçebileceğini söyler. "Aynı anda başlar" demez; "aralarında sıra şartı yok" der. C#'ta Task.WhenAll ile iki Task'ı birlikte beklemek bunun doğal karşılığı.
critical ise bölgenin bölünmez yürüdüğünü söyler; başka bir trace'in mesajları araya giremez. Bu yazının açılışındaki sorun tam bir critical sorusu: bakiye kontrolü ile bakiye düşme arasında başka bir onay araya girerse iki talep aynı günleri harcayabilir. Diyagramda kontrol ve düşmeyi bir critical çerçevesine almak, kodda bir transaction, bir lock ya da LeaveBalance'a eklenecek bir concurrency token (rowversion) demek. Bizim kodumuz bunu henüz yapmıyor; iki onaycı aynı milisaniyede onaylarsa ikisi de kontrolden geçebilir. Çözüm EF Core serisindeki EF Core'da Concurrency: Token, rowversion ve DbUpdateConcurrencyException yazısında. Diyagramın değeri, bu açığı kod yazılmadan konuşulabilir hâle getirmesi.
Asenkron Akış: Queue, Retry ve Timeout

Onay kodundaki son satır PublishAsync, önceki şekilde açık ok ucuyla, yani asynchronous message olarak çizildi. Kodda await var ama beklenen şey yalnız event'in queue'ya bırakılması; e-postanın gitmesi ya da takvime yazılması değil. UML'deki soru "gönderen sonucu bekliyor mu?" olduğu için cevap hayır ve ok ucu açık. Demo'daki queue bir Channel<T>; production'da yerini bir message broker alır, diyagram değişmez:
internal sealed class ChannelEventBus : IIntegrationEventPublisher
{
private readonly Channel<LeaveRequestApproved> channel = Channel.CreateUnbounded<LeaveRequestApproved>();
public ChannelReader<LeaveRequestApproved> Reader => channel.Reader;
public ValueTask PublishAsync(LeaveRequestApproved @event, CancellationToken ct) =>
channel.Writer.WriteAsync(@event, ct);
}
Event'i işleyen BackgroundService, şekildeki fragment'lerin birebir kodu:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (LeaveRequestApproved e in bus.Reader.ReadAllAsync(stoppingToken))
{
await using AsyncServiceScope scope = scopes.CreateAsyncScope();
HrPortalDbContext db = scope.ServiceProvider.GetRequiredService<HrPortalDbContext>();
LeaveRequest request = await db.LeaveRequests
.Include(r => r.Employee).ThenInclude(x => x.Department).ThenInclude(d => d.Director)
.SingleAsync(r => r.Id == e.LeaveRequestId, stoppingToken);
// par: takvim kaydı ve e-posta birbirini beklemez
Task calendarTask = e.NewStatus == LeaveStatus.Approved
? calendar.AddLeaveAsync(request, stoppingToken)
: Task.CompletedTask;
Task mailTask = SendWithRetryAsync(request.Employee.Email,
$"Leave request #{request.Id}: {e.NewStatus}", stoppingToken);
await Task.WhenAll(calendarTask, mailTask);
// opt: direktör onayına düştüyse direktöre de haber ver (R10)
if (e.NewStatus == LeaveStatus.PendingDirectorApproval)
await SendWithRetryAsync(request.Employee.Department.Director.Email,
$"Leave request #{request.Id} waits for your approval", stoppingToken);
}
}
private async Task SendWithRetryAsync(string to, string subject, CancellationToken stoppingToken)
{
for (int attempt = 1; attempt <= MaxAttempts; attempt++)
{
using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
timeout.CancelAfter(AttemptTimeout);
try
{
await mail.SendAsync(to, subject, "", timeout.Token);
return;
}
catch (OperationCanceledException) when (!stoppingToken.IsCancellationRequested)
{
logger.LogWarning("Mail attempt {Attempt} timed out after {Timeout}", attempt, AttemptTimeout);
}
}
logger.LogError("Mail to {To} failed after {Max} attempts", to, MaxAttempts);
}
Timeout'un çizimi için iki seçenek var. Şekildeki {0..2 s} bir duration constraint: iki olay (mesajın gidişi ve cevabın gelişi) arasındaki sürenin sınırı, lifeline'ın yanında bir köşeli ayraçla gösteriliyor. Kodda CancelAfter(AttemptTimeout). Alternatif olarak timeout'u alt ile, "cevap 2 s içinde" ve [else] operand'larıyla iki yol olarak da çizebilirsin; constraint daha kompakt, alt ise timeout'tan sonra ne olduğunu açıkça gösteriyor.
Şekil bir şeyi daha görünür kılıyor: consumer event'leri sırayla işliyor. İlk event'in e-postası 2 saniye timeout beklerken ikinci event (Grace'in onayı) queue'da bekledi; log'daki tüm satırların 23:21:46'da toplanması bu yüzden. Bu bir hata değil, tek consumer'lı bir queue'nun doğası; ama "e-posta karardan sonra gelebilir" sonucunu iş birimine söylemek gerekiyor.
Diyagramlar Kod Olarak: PlantUML ve Mermaid
Onay akışının PlantUML kaynağı; ref over, break, alt ve state invariant için hnote var:
@startuml approve-leave
participant ":LeaveRequestApi" as SPA
participant ":HrPortal.Api" as Api
participant ":LeaveRequestService" as Svc
participant ":ILeaveRequestRepository" as Repo
participant "req42 : LeaveRequest" as Req
participant ":IBalanceService" as Bal
queue ":IIntegrationEventPublisher" as Bus
SPA -> Api : POST /leave-requests/42/approve
activate Api
Api -> Svc : ApproveAsync(42, approverId, comment)
activate Svc
ref over Svc, Repo : Load request and approver
Svc -> Bal : HasEnoughBalanceAsync(4, annual, 2026, 5)
Bal --> Svc : enough
break enough == false
Svc --> Api : LeaveRuleException
Api --> SPA : 409 Conflict
end
Svc -> Req : Approve(approver, comment, now)
alt step == Manager and RequiresDirectorApproval
hnote over Req : PendingDirectorApproval
else else
hnote over Req : Approved
Svc -> Bal : DeductAsync(4, annual, 2026, 5)
end
Svc -> Repo : SaveChangesAsync()
Svc ->> Bus : PublishAsync(LeaveRequestApproved)
Svc --> Api : req42
deactivate Svc
Api --> SPA : 200 OK
deactivate Api
@enduml
PlantUML'de -> synchronous call, ->> ince açık ok ucuyla asynchronous message. Consumer tarafının Mermaid kaynağı; -) asynchronous message, par ... and ... end paralel operand'lar:
sequenceDiagram
participant Bus as :Channel
participant C as :LeaveApprovedConsumer
participant Cal as :ICalendarGateway
participant Mail as :INotificationSender
Bus-)C: LeaveRequestApproved(42, Director, Approved)
activate C
par calendar
opt NewStatus == Approved
C->>Cal: AddLeaveAsync(req42)
Cal-->>C: done
end
and mail to requester
loop attempt 1..3, until sent
C->>Mail: SendAsync(ada@hrportal.test) within 2 s
Mail-->>C: sent or timeout
end
end
opt NewStatus == PendingDirectorApproval
C->>Mail: SendAsync(grace@hrportal.test)
end
deactivate C
İki dosya da hatasız render oldu, ama iki küçük sürprizle. Mermaid 12.3.0'da ref over parse hatası veriyor; interaction use için PlantUML gerekiyor. Mermaid mesaj metninde # karakterini özel karakter olarak yorumladı ve "#42: Approved" etiketini yarıda kesti; metni değiştirmek zorunda kaldım. Duration constraint'in ikisinde de doğrudan karşılığı yok, metin olarak yazılıyor.
Kardeşleri: Communication ve Timing Diagram
Communication diagram aynı interaction'ı zaman ekseni olmadan, object'ler arası bağlantılar üzerine numaralı mesajlarla (1, 1.1, 1.2...) çizer. Sıra numaralardan okunur; kazanç, kimin kiminle konuştuğunun bir ağ olarak görünmesi. Az mesajlı ama çok katılımcılı akışlarda, örneğin hangi service'in hangi dış sisteme bağlı olduğunu göstermek için işe yarar; fragment'leri ifade edemediği için koşullu akışlarda sequence diagram'ın gerisinde kalır.
Timing diagram ise yatay zaman ekseninde her lifeline'ın durumunu gösterir; SLA (Service Level Agreement) ve timeout konuşurken eşsizdir. R10'daki "onaycı 2 iş günü içinde karar vermezse hatırlatma gönderilir" kuralı için req42'nin PendingManagerApproval → PendingDirectorApproval → Approved çizgisinin altına {≤ 2 iş günü} constraint'leri koyan bir timing diagram, bir sequence diagram'dan çok daha net konuşur. İkisini de ihtiyaç doğduğunda çiz; günlük işin yüzde doksanı sequence diagram.
Sık Düşülen Tuzaklar
alt ile break'i karıştırmak. Hata yolunu alt'ın bir operand'ı olarak çizersen okuyucu iki yolun sonra birleştiğini sanır. Akış orada bitiyorsa break.
Guard'ı belirsiz yazmak. [hata] ya da [gerekirse] bir koşul değil. Guard, o noktadaki bilgiyle değerlendirilebilen bir ifade olmalı: [enough == false], [NewStatus = Approved].
Fragment'leri iç içe sınırsız dallandırmak. Üç seviye iç içe alt gördüğünde diyagram koda dönüşmüş demektir; ayrı senaryolara böl ya da ref ile parçala.
await edildiği için queue'ya yazmayı synchronous çizmek. Sync ya da async kararını, gönderenin işin sonucunu bekleyip beklemediğine göre ver; await anahtar kelimesine göre değil.
par'ı "aynı anda" sanmak. par sıralama garantisi olmadığını söyler. Takvim kaydı e-postadan önce de bitebilir, sonra da; test ve log beklentilerini buna göre kur.
Küçük Bir Deneme
★☆☆ Şekildeki onay akışına, onaycı talebin yöneticisi ya da direktörü değilse 403 Forbidden dönen bir break ekle. Bu kontrol ref çerçevesinin içinde mi olmalı, dışında mı?
★★☆ Bakiye kontrolü ile bakiye düşmeyi bir critical fragment'ine al, sonra bunu kodda LeaveBalance'a bir rowversion ekleyerek gerçekleştir. İki onayı paralel çalıştıran bir test yaz ve bir tanesinin DbUpdateConcurrencyException aldığını göster.
★★★ R10'daki hatırlatma kuralı için bir timing diagram çiz: yatay eksende iş günleri, dikeyde req42'nin durumları ve hatırlatma e-postasının gittiği an. Sonra bu kuralı bir BackgroundService ve TimeProvider ile yaz ve FakeTimeProvider ile test et.
Bir sonraki yazıda aynı onay sürecine bu kez nesneler arası mesajlar yerine iş akışı olarak bakıyoruz: activity diagram ile çalışan, sistem, yönetici ve direktör swimlane'lerini, decision ve fork/join node'larını ve join'in neden beklediğini token akışıyla çizeceğiz.