Activity Diagram: Action, Decision, Fork/Join, Swimlane ve Onay İş Akışını Modellemek
Activity diagram bir iş akışını adımlar, guard'lı decision'lar, fork/join ile paralellik ve kimin yaptığını gösteren swimlane'lerle çizer; semantiği token akışıdır, bu yüzden decision'dan çıkan yollar bir merge'de birleşmezse akış implicit join'de takılır. Yazıda İK Portalı'nın izin onay süreci swimlane'lerle çiziliyor, token kuralları küçük bir C# simülatörüyle gösteriliyor ve akış handler'lara bölünüyor.
Sürüm notu: Bu yazıdaki diyagramlar UML (Unified Modeling Language) 2.5.1 notasyonuyla çizilmiştir; C# örnekleri .NET 10 ile, PlantUML kodu PlantUML 1.2026.9 ile denenmiştir.
Önce Tahmin Et: #43 Neden Hiç Onaylanmadı?
İK (insan kaynakları) Portalı'nın analisti onay sürecini beyaz tahtaya çizmiş: yönetici talebi inceliyor, bir decision node "3 iş gününden uzun mu?" diye soruyor. Evetse akış direktörün incelemesine gidiyor; değilse doğrudan "Onaylandı say" action'ına. Direktörün incelemesinden çıkan ok da aynı "Onaylandı say" kutusuna giriyor. Kutuya iki ok giriyor, ikisi de aynı yere çıkıyor, çizim temiz görünüyor.
Ekip diyagramı doğrulamak için UML activity semantiğinin küçük bir modelini (bir "token game") yazmış; çizimdeki edge'ler birebir şöyle:
int workingDays = 2; // #43: 2 iş günü, direktör onayı gerekmez
new TokenGame([
new(start, review), new(review, needsDirector),
new(needsDirector, directorReview, () => workingDays > 3), new(needsDirector, markApproved, () => workingDays <= 3),
new(directorReview, markApproved), .. tail
]).Run(start);
Ada'nın iki günlük mazeret izni #43 bu akıştan geçince ne olur? "Onaylandı say" çalışır mı? Çalışmazsa token nerede kalır?
Cevap: Token Takılır
== without merge ==
fire ManagerReview
fire NeedsDirector?
stuck: 1 token(s) waiting on NeedsDirector? -> MarkApproved
"Onaylandı say" hiç çalışmadı. UML 2.5.1'in Activities bölümüne göre, birden fazla gelen control flow edge'i olan bir action, hepsinden birer token gelmeden başlamaz; yani o kutu gizlice bir join gibi davranır (implicit join). Decision ise token'ı yalnız bir edge'e verir. Sonuç: bir edge'de token var, öbüründe yok, action sonsuza kadar bekliyor. Aynı modele iki yolu bir merge node ile birleştirip verince akış tamamlanıyor:
== with merge ==
fire ManagerReview
fire NeedsDirector?
fire Merge
fire MarkApproved
fire Fork
fire DeductBalance
fire AddToCalendar
fire Join
fire end
activity finished

Gerçek hayatta bu hata kodda değil, diyagramı okuyan insanların kafasında olur: biri "iki ok giriyorsa ikisini de bekler" diye anlar, biri "hangisi gelirse" diye. Activity diagram'ın notasyonu, tam bu tür belirsizlikleri ortadan kaldırmak için bir semantiğe sahip: token akışı. Bu yazıda o semantiği, notasyonu ve izin onay sürecinin swimlane'li diyagramını çizip koda nasıl bölündüğüne bakacağız.
Activity Diagram Neyi Cevaplar?
Sequence diagram "bu senaryoda kim kime hangi sırayla mesaj gönderiyor?" sorusunu, state machine diagram "bir object hangi durumlardan geçiyor?" sorusunu cevaplar. Activity diagram ise "iş hangi adımlardan, hangi koşullarla, hangi paralellikle ilerliyor ve her adımı kim yapıyor?" sorusunu. Odakta object'ler değil, işin kendisi var. Bu yüzden iş birimiyle bir süreci konuşmak, bir algoritmanın akışını göstermek ya da günler süren, birden fazla kişinin dokunduğu bir iş akışını tek sayfada görmek için doğru araç.
UML 2 ile birlikte activity'nin semantiği Petri net'lere benzeyen bir token modeline oturtuldu: node'lar token alır ve verir, edge'ler token taşır. Bu semantik, diyagramın "yaklaşık bir çizim" değil, çalıştırılabilir kadar kesin bir tanım olmasını sağlıyor; açılıştaki simülatör de tam bu kurallarla yazıldı.
Notasyon

Initial node (dolu daire) akışı başlatır. Action (yuvarlak köşeli dikdörtgen) tek bir iş adımıdır. Decision node (elmas) bir gelen edge'i, guard'lı birden çok giden edge'e ayırır; token yalnız guard'ı doğru olan edge'den geçer. Guard'lar birbirini dışlamalı ve tüm durumları kapsamalı, bu yüzden [else] çok kullanışlı. Merge node da bir elmastır ama tersini yapar: birden çok gelen edge'den gelen her token'ı tek çıkışa geçirir, beklemez.
Fork node (kalın çubuk) gelen token'ı her giden edge için çoğaltır; dallar artık paraleldir. Join node (yine kalın çubuk) tüm gelen edge'lerde token olmasını bekler, sonra tek bir token çıkarır. Activity final (içi dolu halkalı daire) ilk token geldiği anda bütün activity'yi bitirir; diğer dallardaki token'lar da yok olur. Flow final (içinde çarpı olan daire) yalnız gelen token'ı yok eder, diğer dallar devam eder.
Object node (düz dikdörtgen) akan veriyi gösterir; köşeli parantez içinde durumu yazılabilir: LeaveRequest [PendingManagerApproval]. Send signal action (dışa dönük beşgen) bir sinyal gönderir, accept event action (içe oyuk beşgen) bir sinyal bekler, accept time event (kum saati) belli bir süre ya da zaman gelince tetiklenir. Activity partition (swimlane), adımları kimin yaptığına göre gruplayan şeritlerdir.
İzin Onay İş Akışı: Swimlane'lerle

Şekil, requirement'lardaki R1–R10 maddelerinin onay kısmı. Dört partition var: Çalışan, İK Portalı (sistem), Yönetici ve Direktör. Akış Çalışan şeridinde başlıyor, sistem bakiye ve çakışma kontrolü yapıyor (R3, R4); kontrol başarısızsa çalışan hatayı görüyor ve activity bitiyor. Başarılıysa ortaya bir object node çıkıyor: PendingManagerApproval durumundaki talep. Yönetici talebi inceliyor; ret kolu gerekçe yazmayı içeriyor (R6) ve kendi activity final'ında bitiyor.
Onay kolu sisteme dönüyor ve ikinci decision soruluyor: [WorkingDays > 3] ise talep Direktör şeridine geçiyor (R7). Direktörün onayı ile "direktör gerekmiyor" kolu bir merge'de birleşiyor; açılıştaki hatanın düzeltilmiş hâli tam bu nokta. Ardından bir fork üç paralel dal açıyor: bakiyeden düş, takvime işle ve çalışana bildir (R9, R10). Join üçünü bekliyor, sonra activity bitiyor.
Yönetici şeridinin üstündeki kum saati, R10'daki hatırlatma kuralı: gelen edge'i olmayan bir accept time event, activity başladığında etkinleşir ve süre dolunca tetiklenir. Arkasından gelen "Hatırlat" sinyali gönderilince akış bir flow final'da bitiyor. Activity final kullansaydık hatırlatma, onay sürecinin tamamını öldürürdü; flow final yalnız kendi token'ını yok ediyor.
İki bilinçli eksik var. İK uzmanı için şerit yok, çünkü bu süreçte bir adımı yok; İK, iptal (R11) ve bakiye düzeltme (R12) süreçlerinde yer alır. Boş bir swimlane çizmek okuyucuya "burada bir şey olmalıydı" dedirtir. Onay anındaki bakiye kontrolü (R8) de çizilmedi; serinin önceki yazısındaki sequence diagram'da ayrıntısıyla vardı. Activity diagram sürecin iskeletini, sequence diagram bir adımın içini gösteriyor.
Token Akışı: Join Neden Bekler?
Activity semantiğini kavramanın en kısa yolu, onu bir kez kendin çalıştırmak. Açılıştaki simülatörün çekirdeği şu:
public enum NodeKind { Initial, Action, Decision, Merge, Fork, Join, ActivityFinal }
public sealed record Node(string Name, NodeKind Kind);
public sealed record Edge(Node From, Node To, Func<bool>? Guard = null);
// UML activity semantiğinin küçük bir modeli: token'lar edge'lerde bekler, node'lar kuralına göre ateşlenir
public sealed class TokenGame(IReadOnlyList<Edge> edges)
{
private readonly Dictionary<Edge, int> tokens = edges.ToDictionary(e => e, _ => 0);
public void Run(Node initial)
{
Offer(initial);
bool fired = true;
while (fired)
{
fired = false;
foreach (Node node in edges.Select(e => e.To).Distinct())
{
List<Edge> incoming = edges.Where(e => e.To == node).ToList();
// Action ve join: her gelen edge'de token olmalı (action için implicit join)
bool ready = node.Kind is NodeKind.Action or NodeKind.Join or NodeKind.Fork or NodeKind.ActivityFinal
? incoming.All(e => tokens[e] > 0)
: incoming.Any(e => tokens[e] > 0);
if (!ready) continue;
foreach (Edge e in node.Kind is NodeKind.Merge or NodeKind.Decision
? [incoming.First(e => tokens[e] > 0)]
: incoming)
tokens[e]--;
Console.WriteLine($"fire {node.Name}");
if (node.Kind == NodeKind.ActivityFinal)
{
Console.WriteLine("activity finished");
return;
}
Offer(node);
fired = true;
}
}
foreach ((Edge e, int count) in tokens.Where(t => t.Value > 0))
Console.WriteLine($"stuck: {count} token(s) waiting on {e.From.Name} -> {e.To.Name}");
}
private void Offer(Node node)
{
List<Edge> outgoing = edges.Where(e => e.From == node).ToList();
IEnumerable<Edge> targets = node.Kind == NodeKind.Decision
? [outgoing.First(e => e.Guard?.Invoke() ?? true)]
: outgoing;
foreach (Edge e in targets)
tokens[e]++;
}
}
Kurallar dört satıra sığıyor. Action ve join, gelen her edge'de token ister; merge ve decision bir token'la yetinir. Decision token'ı guard'ı doğru olan tek edge'e verir; diğer node'lar (fork dahil) her giden edge'e birer token verir. Activity final ilk token'da her şeyi bitirir. "With merge" çıktısında Fork'tan sonra DeductBalance ve AddToCalendar ayrı ayrı ateşleniyor, Join ancak ikisi de bitince. Bu model gerçek UML semantiğinin basitleştirilmiş hâli (object flow, weight, interruptible region yok), ama join'in neden beklediğini ve merge'süz birleşmenin neden takıldığını göstermeye yetiyor.
Fork/join'in C#'taki karşılığını zaten yazmıştık: önceki yazıdaki LeaveApprovedConsumer, takvim kaydını ve e-postayı iki Task olarak başlatıp await Task.WhenAll(calendarTask, mailTask) ile bekliyor. Task.WhenAll bir join: tüm task'lar bitmeden devam etmez.
Koda Dönüş: İş Akışını Handler'lara Bölmek
Şekildeki activity günler sürüyor ve dört farklı kişiye dokunuyor; bunu tek bir method olarak yazamazsın. Diyagramı koda dökerken asıl karar, akışın nerelerde "durup beklediği". Her bekleme noktası (bir insanın kararı, bir zamanlayıcı, bir dış sistem) bir sınır demek: sınırın öncesi bir handler, sonrası başka bir handler, aradaki durum ise veritabanında saklanan bir object node.
| Diyagramdaki bölüm | Koddaki karşılığı |
|---|---|
| Talebi gönder + kontrol + decision | LeaveRequestService.CreateAsync ve SubmitAsync, hata kolu 409 |
Object node [PendingManagerApproval] | LeaveRequest.Status, SaveChangesAsync ile kalıcı |
| Yönetici ve Direktör kararları | POST /leave-requests/{id}/approve (ve /reject), ApproveAsync |
[WorkingDays > 3] decision ve merge | LeaveRequest.Approve içindeki durum seçimi |
| Fork, üç paralel dal, join | LeaveApprovedConsumer, Task.WhenAll |
| Accept time event + Hatırlat + flow final | PeriodicTimer ile çalışan bir BackgroundService (henüz yazılmadı, alıştırma) |
Tablonun gösterdiği şey önemli: "akış nerede kaldı?" sorusunun cevabı koddaki bir değişkende değil, Status kolonunda. Activity diagram'ın object node'ları, bir sonraki yazının konusu olan state machine diagram'ın durumlarına dönüşüyor. Bu tür uzun süren, insan adımlı akışlar için ayrı workflow engine'ler de var; bu seride kendi handler'larımızla kalıyoruz, çünkü akış küçük ve her adımı test edilebilir.
Diyagram Kod Olarak: PlantUML
Swimlane'li activity diagram'ın PlantUML kaynağı. Partition'lar |Ad| satırlarıyla, fork'lar fork, fork again ve end fork ile yazılıyor:
@startuml leave-approval-activity
|Employee|
start
:Submit leave request;
|HR Portal|
:Check balance and overlaps;
if (valid?) then (no)
|Employee|
:See the error;
stop
else (yes)
|HR Portal|
:LeaveRequest [PendingManagerApproval]; <<object>>
endif
|Manager|
:Review request;
if (decision?) then (reject)
:Reject with reason;
stop
else (approve)
endif
|HR Portal|
if (WorkingDays > 3?) then (yes)
|Director|
:Review request;
if (decision?) then (reject)
:Reject with reason;
stop
else (approve)
endif
else (no)
endif
|HR Portal|
fork
:Deduct balance;
fork again
:Add to calendar;
fork again
:Notify employee; <<sendSignal>>
end fork
stop
@enduml
Bu dosya PlantUML 1.2026.9 ile render oldu; endif'ten sonra PlantUML merge elmasını kendisi çiziyor. Yolda bir sürprizle karşılaştım: PlantUML belgelerinde eski SDL (Specification and Description Language) kısaltmaları (:Notify employee> gibi satırın sonundaki karakterle şekil seçmek) artık deprecated olarak listeleniyor ve bu sürümde render hatası verdi. Doğru yol <<object>>, <<sendSignal>>, <<acceptEvent>> ve <<timeEvent>> stereotype'ları. Hatırlatma kuralını PlantUML'de çizmedim; bağımsız bir accept time event'i bu söz dizimiyle akıştan ayrı göstermek zor ve diyagramı okunmaz yapıyor.
Flowchart ve BPMN ile Farkı
Activity diagram ilk bakışta bir flowchart'a benzer, ama iki önemli fark var. Flowchart'ın resmi bir semantiği yoktur: iki okun bir kutuda birleşmesinin ne demek olduğunu kimse tanımlamamıştır. Activity diagram'da tanımlıdır (implicit join), açılıştaki hata da zaten bundan çıktı. İkinci fark paralellik: flowchart'ta fork ve join yoktur.
BPMN (Business Process Model and Notation) ise yine OMG'nin (Object Management Group) yönettiği, iş süreçlerine özel bir notasyon: pool ve lane'ler, zengin event türleri (timer, message, error), gateway'ler ve organizasyonlar arası mesaj akışları. İş birimi süreç modellemesine alışkınsa ya da süreç bir BPMN engine'inde çalıştırılacaksa BPMN daha doğru seçim. BPMN'i ilerideki Requirements Analysis serisinde ele alacağız. Yazılımcılar arasında, kodla eşleşen bir iş akışını konuşmak için UML activity diagram yeterli ve daha hafif. Activity notasyonunun bir de kardeşi var: düğümleri birer interaction (ref çerçevesi) olan interaction overview diagram; önceki yazıdaki "her adım ayrı bir sequence diagram" fikrinin üst seviye haritası.
Sık Düşülen Tuzaklar
Decision ile fork'u karıştırmak. "Yönetici onaylar ya da reddeder" bir decision'dır, bir fork değil. Fork çizersen iki dal da çalışır: talep hem onaylanır hem reddedilir.
Merge'süz birleşen akış. Açılıştaki hata: decision'dan çıkan iki yolu doğrudan bir action'a bağlamak, implicit join yüzünden akışı kilitler. Alternatif yollar bir merge'de birleşir; paralel yollar bir join'de.
Flow final yerine activity final kullanmak. Paralel bir daldaki "bitti" işareti activity final ise, o dal bittiği anda diğer bütün dallar da durur. Hatırlatma, loglama gibi yan dallar flow final ile biter.
Guard'ları çakışan ya da eksik yazmak. [WorkingDays > 3] ve [WorkingDays < 3] yazarsan tam 3 günlük talep hiçbir yoldan geçemez. [else] kullan.
Swimlane'leri sistemlere göre değil kişilere göre çizmeyi unutmak ya da her sistemi ayrı şerit yapmak. Partition sorumluluğu gösterir; "Veritabanı" bir şerit değildir. Boş şerit de çizme.
Küçük Bir Deneme
★☆☆ Şekle R11'deki iptal akışını ekle: çalışan onay bekleyen talebini her an iptal edebiliyor. Bunu hangi notasyonla çizerdin? (İpucu: interruptible activity region ve accept event action.)
★★☆ TokenGame'e FlowFinal node türünü ekle ve hatırlatma dalını (accept time event yerine ayrı bir initial'dan başlayan bir dal) modelle. Hatırlatma dalını yanlışlıkla activity final ile bitirince ana akışın token'larına ne olduğunu çıktıyla göster.
★★★ Hatırlatma kuralını gerçek kod olarak yaz: TimeProvider ve PeriodicTimer kullanan bir BackgroundService, 2 iş gününü (hafta sonu ve PublicHoliday hariç) aşan bekleyen onayları bulup onaycıya e-posta göndersin. Activity diagram'daki hangi node'un kodda hangi satıra karşılık geldiğini yorumlarla işaretle.
Bir sonraki yazıda bu yazıdaki object node'ların izini sürüyoruz: state machine diagram ile LeaveRequest'in Taslak'tan Onaylandı, Reddedildi ve İptal Edildi'ye kadar bütün durumlarını, geçişlerin trigger [guard] / effect söz dizimini ve bunları C#'ta switch expression'a ve Stateless kütüphanesine dökmeyi göreceğiz.