Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı UML ve Modeling — #09

Sequence Diagram 2: alt, opt, loop, par, break ve ref ile Hata Yolları ve Asenkron Akışlar

Mert Özen 7 Kas 2026 12 dk 5 görüntülenme
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ı:

OperatorAnlamıC# karşılığı
altGuard'ı doğru olan tek operand çalışırif / else, switch
optTek operand; guard doğruysa çalışırelse'siz if
breakGuard doğruysa operand çalışır, çevreleyen akışın kalanı atlanırthrow, erken return
loopOperand en az min, en çok max kez tekrarlanırfor, while, retry döngüsü
parOperand'lar birbirinden bağımsız, iç içe geçebilirTask.WhenAll
criticalBölge araya başka bir şey girmeden yürürlock, transaction, concurrency token
ref (interaction use)Başka bir interaction'ı buraya çağırırayrı 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

09-approve-fragments-tr.png

Ş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

09-async-consumer-tr.png

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.