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

Uçtan Uca Modelleme: İzin Modülünü Requirement'tan Çalışan Koda Taşımak

Mert Özen 7 Kas 2026 17 dk 3 görüntülenme
Uçtan Uca Modelleme: İzin Modülünü Requirement'tan Çalışan Koda Taşımak

Diyagramlar, requirement'tan endpoint'e, state machine'den domain koduna ve ER'den migration'a izlenebilir bir zincir kurduğunda ve değişiklikten önce "hangi diyagram değişiyor?" sorusunu cevaplamak için kullanıldığında değer üretir. Serinin finalinde İzin modülünü .NET 10 API, EF Core ve Angular 22 koduna taşıyor, yarım gün izin requirement'ının etkisini git diff'le ölçüyor ve yol boyunca diyagramların yakaladığı gerçek hataları gösteriyoruz.

Sürüm notu: Bu yazıdaki diyagramlar UML (Unified Modeling Language) 2.5.1, C4 (Context, Containers, Components, Code) ve Crow's Foot notasyonuyla çizilmiştir; C# örnekleri .NET 10, Entity Framework Core (EF Core) 10.0.14 ve veritabanı sunucusu (SQL Server 2025) ile, Angular kodu Angular 22.2 ile derlenip denenmiştir.

Önce Tahmin Et: Yarım Gün İzin Hangi Diyagramları Değiştirir?

Seri boyunca İK (insan kaynakları) Portalı'nın İzin modülü için altı diyagram çizdik: use case, domain class diagram, "talebi gönder" sequence diagram'ı, state machine, ER (Entity-Relationship) diagram ve C4 container diyagramı. Bu yazıda hepsini tek bir repo'da çalışan koda bağlayacağız. Sonra iş biriminden yeni bir madde gelecek:

- R13. A single-day leave can be requested as a half day (morning or afternoon); it counts as 0.5 working day.

Kodu açmadan önce tahmin et: altı diyagramdan hangileri değişir, hangileri olduğu gibi kalır? Yeni bir use case mi gerekir, state machine'e yeni bir durum mu eklenir, C4'te yeni bir container mı çıkar? Ve veritabanında zaten duran 42, 43 ve 44 numaralı talepler bu değişiklikten nasıl etkilenir? Cevabı yazının sonunda gerçek git diff çıktısıyla vereceğiz.

Başlangıç: Requirement Metni ve Repo Düzeni

Her şey serinin 2. yazısındaki requirement metniyle başladı. Repo'da onun kısaltılmış İngilizce hali docs/requirements.md olarak duruyor ve maddeler R1, R2 diye numaralı; bu numaralar diyagramlarda, kod yorumlarında ve PR'larda (pull request) izlenebilirliğin anahtarı:

# Leave Request Module - Business Requirements (v1.0)

- R1. An employee creates a leave request: leave type, start and end date, optional reason. Draft first, then submit.
- R2. On submission the system counts working days (weekends and HR-defined public holidays excluded).
- R3. Annual leave cannot be submitted if the remaining balance is less than the requested working days.
- R4. An employee cannot have two active requests with overlapping dates.
- R6. A submitted request goes to the line manager, who approves or rejects (a reason is mandatory when rejecting).
- R7. Requests longer than 3 working days also need the department director's approval.
- R8. The balance is checked again at approval time.
- R9. Final approval deducts annual leave from the balance and writes the leave to the calendar system.
- R11. Pending requests can be cancelled any time; approved ones only before they start (HR can cancel later).

Repo düzeni, diyagramın kodla aynı yerde yaşaması fikrine göre kuruldu:

docs/
    requirements.md
    diagrams/
        hr-style.iuml
        use-cases.puml
        domain-model.puml
        submit-leave-request.puml
        leave-request-states.puml
        containers.puml
        generated/
            er.mmd
src/
    HrPortal.Leave/          Domain/, Application/, Infrastructure/
    HrPortal.Api/            Program.cs, LeaveEndpoints.cs, Migrations/
web/
    hr-portal-web/           Angular 22
.github/
    pull_request_template.md
    workflows/diagrams.yml

İki tür diyagram var ve ayrım bilinçli. docs/diagrams/*.puml elle çizilir, çünkü bir karar ya da bir soru anlatır: hangi actor neyi yapar, hangi durumdan hangisine geçilir. docs/diagrams/generated/er.mmd ise koddan üretilir, çünkü cevabı zaten EF Core modelinde: hangi tabloda hangi kolon var. Elle çizilen diyagramı review'da okursun, üretileni CI (Continuous Integration) içinde "güncel mi?" diye kontrol edersin. Kısalık için Domain, Application ve Infrastructure tek bir project'in üç namespace'i; 7. yazıdaki package diagram'ı onları ayrı project'lere nasıl böleceğini anlatıyordu. Bütün parçaların birbirine bağlandığı harita:

18-traceability-map-tr.png

Use Case'ten Endpoint'e

Use case diagram sistemin dışarıya ne sunduğunu söyler; API'de (Application Programming Interface) bunun karşılığı endpoint'lerdir. Eşleme birebir değil ama izlenebilir olmalı:

Use caseHTTP (Hypertext Transfer Protocol) endpointRequirement
UC-01 İzin Talep EtPOST /leave-requests, POST /leave-requests/{id}/submitR1–R4
UC-02 Talebi Karara BağlaPOST /leave-requests/{id}/approve, POST /leave-requests/{id}/rejectR6–R9
UC-03 Talebi İptal EtPOST /leave-requests/{id}/cancelR11
UC-04 Bakiyeyi GörüntüleGET /employees/{number}/leave-balancesR3

UC-01 iki endpoint'e bölündü, çünkü requirement taslak ile gönderimi ayırıyor ve state machine'de bu iki ayrı durum (Draft ve PendingManagerApproval). Minimal API ile:

public static class LeaveEndpoints
{
    // Use case -> endpoint eşlemesi (UC-01, UC-02, UC-03, UC-04)
    public static void MapLeaveEndpoints(this IEndpointRouteBuilder app)
    {
        var requests = app.MapGroup("/leave-requests");

        requests.MapPost("/", async (CreateLeaveRequest body, ILeaveRequestService service, CancellationToken ct) =>
        {
            var dto = await service.CreateAsync(body, ct);
            return TypedResults.Created($"/leave-requests/{dto.Id}", dto);
        });
        requests.MapPost("/{id:int}/submit", (int id, ILeaveRequestService s, CancellationToken ct) => s.SubmitAsync(id, ct));
        requests.MapPost("/{id:int}/approve", (int id, DecisionRequest body, ILeaveRequestService s, CancellationToken ct) => s.ApproveAsync(id, body, ct));
        requests.MapPost("/{id:int}/reject", (int id, DecisionRequest body, ILeaveRequestService s, CancellationToken ct) => s.RejectAsync(id, body, ct));
        requests.MapPost("/{id:int}/cancel", (int id, CancelRequest body, ILeaveRequestService s, CancellationToken ct) => s.CancelAsync(id, body, ct));
        requests.MapGet("/{id:int}", async (int id, ILeaveRequestService s, CancellationToken ct) =>
            await s.FindAsync(id, ct) is { } dto ? Results.Ok(dto) : Results.NotFound());

        app.MapGet("/employees/{number}/leave-balances", (string number, HrPortalDbContext db, CancellationToken ct) =>
            db.LeaveBalances
                .Where(b => db.Employees.Any(e => e.Id == b.EmployeeId && e.EmployeeNumber == number))
                .Join(db.LeaveTypes, b => b.LeaveTypeId, t => t.Id,
                    (b, t) => new LeaveBalanceDto(t.Code, b.Year, b.EntitledDays, b.UsedDays, b.EntitledDays - b.UsedDays))
                .ToListAsync(ct));
    }
}

Use case'lerden ikisi (UC-05 bakiye düzeltme ve UC-06 izin kuralları) bu iterasyonun dışında kaldı. Bunu kodda değil diyagramda görmek değerli: use case diagram, "henüz yapılmamış" olanı da gösteren tek yer.

Domain Model ve State Machine'den Domain Koduna

Domain class diagram'ındaki LeaveRequest class'ı attribute'larıyla, operation'larıyla ve DirectorApprovalThreshold = 3 sabitiyle koda geçiyor. State machine'deki her ok bir method'un içinde bir geçiş; geçersiz her ok bir exception:

public class LeaveRequest
{
    public const int DirectorApprovalThreshold = 3;
    private readonly List<Approval> _approvals = [];

    // ...

    public bool RequiresDirectorApproval => WorkingDays > DirectorApprovalThreshold;

    // Draft --submit--> PendingManagerApproval
    public void Submit(int workingDays, DateTimeOffset now)
    {
        if (Status != LeaveStatus.Draft) throw new InvalidLeaveTransitionException(Status, "submit");
        WorkingDays = workingDays;
        SubmittedAt = now;
        Status = LeaveStatus.PendingManagerApproval;
    }

    // PendingManagerApproval --approve [WorkingDays > 3]--> PendingDirectorApproval
    // PendingManagerApproval --approve [WorkingDays <= 3]--> Approved
    // PendingDirectorApproval --approve--> Approved
    public void Approve(Employee approver, string? comment, DateTimeOffset now)
    {
        var step = CurrentStep("approve");
        _approvals.Add(new Approval(approver.Id, step, ApprovalDecision.Approved, comment, now));
        Status = step == ApprovalStep.Manager && RequiresDirectorApproval
            ? LeaveStatus.PendingDirectorApproval
            : LeaveStatus.Approved;
    }

    private ApprovalStep CurrentStep(string trigger) => Status switch
    {
        LeaveStatus.PendingManagerApproval => ApprovalStep.Manager,
        LeaveStatus.PendingDirectorApproval => ApprovalStep.Director,
        _ => throw new InvalidLeaveTransitionException(Status, trigger)
    };
}

Composition'lar (LeaveRequest *-- Approval) koleksiyonun sahipliğine dönüşüyor: _approvals dışarıdan değiştirilemez, onay yalnız Approve ve Reject ile eklenir. Department.DirectorId'nin int? olduğuna da dikkat et; ER diagram yazısında iki zorunlu ilişkinin döngüsünün ilk satırı eklemeyi imkânsız kıldığını görmüş ve bu ucu optional yapmıştık. Seed kodu da o yazıdaki iki adımlı eklemeyi kullanıyor.

Sequence Diagram'dan Service'e

Sequence diagram'daki lifeline'lar class'lara, mesajlar method çağrılarına dönüşüyor: HrPortal.Api endpoint, LeaveRequestService.SubmitAsync, IWorkingDayCalculator.CountWorkingDaysAsync, IBalanceService.HasEnoughBalanceAsync. Kodu diyagramdaki sırayla yazdım ve ilk sürümün davranışını uçtan uca bir script'le denedim. Onaylanmış #42 numaralı talep için submit tekrar çağrıldığında cevap şuydu (traceId kısaltıldı):

POST /leave-requests/42/submit -> 422
{"type":"https://tools.ietf.org/html/rfc4918#section-11.2","title":"Rule violated: balance-exceeded","status":422,"detail":"Not enough leave balance.","traceId":"00-13ca5c9c..."}

Talep zaten onaylanmış ve bakiyeden düşülmüş; ikinci gönderimde bakiye yetmediği için 422 dönüyor. Oysa state machine'e göre Approved durumundan Submit diye bir geçiş yok; doğru cevap "geçersiz geçiş" olmalıydı. Hata koddan değil diyagramdan geliyordu: sequence diagram'da talep yüklendikten hemen sonra iş günü hesabı ve bakiye kontrolü geliyordu, durum kontrolü ise en sonda request.Submit içindeydi. Düzeltme hem kodda hem diyagramda aynı satır oldu:

 Service -> Db : load LeaveRequest #42
+Service -> Service : guard Status == Draft (else 409)
 Service -> Calc : CountWorkingDays(2026-12-21..25)
public async Task<LeaveRequestDto> SubmitAsync(int id, CancellationToken ct)
{
    var request = await LoadAsync(id, ct);
    // Önce state machine: Draft dışındaki her durumda submit geçersiz (409), kurallara bakmaya gerek yok
    if (request.Status != LeaveStatus.Draft) throw new InvalidLeaveTransitionException(request.Status, "submit");

    var overlaps = await db.LeaveRequests
        .Where(r => r.EmployeeId == request.EmployeeId && r.Id != request.Id)
        .Where(r => r.Status != LeaveStatus.Draft && r.Status != LeaveStatus.Rejected && r.Status != LeaveStatus.Cancelled)
        .Where(r => r.Period.Start <= request.Period.End && request.Period.Start <= r.Period.End)
        .AnyAsync(ct);
    if (overlaps) throw new LeaveRuleViolationException("overlap", "Another active request overlaps these dates.");

    var days = await workingDays.CountWorkingDaysAsync(request.Period, ct);
    if (request.LeaveType.DeductsFromBalance &&
        !await balances.HasEnoughBalanceAsync(request.EmployeeId, request.LeaveType, request.Period.Start.Year, days, ct))
        throw new LeaveRuleViolationException("balance-exceeded", "Not enough leave balance.");

    request.Submit(days, clock.GetUtcNow());
    await db.SaveChangesAsync(ct);
    return await LoadDtoAsync(id, ct);
}

Hangi hatanın hangi HTTP status'una gittiğini de diyagramlar belirliyor. State machine'de olmayan bir geçiş 409 Conflict, requirement'taki bir kuralın ihlali 422 Unprocessable Entity, olmayan kayıt 404:

// State machine'de olmayan geçiş -> 409, iş kuralı ihlali -> 422, kayıt yok -> 404
public sealed class LeaveExceptionHandler(IProblemDetailsService problems) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext http, Exception exception, CancellationToken ct)
    {
        (int status, string title) = exception switch
        {
            InvalidLeaveTransitionException => (StatusCodes.Status409Conflict, "Invalid status transition"),
            LeaveRuleViolationException e => (StatusCodes.Status422UnprocessableEntity, $"Rule violated: {e.Rule}"),
            NotFoundException => (StatusCodes.Status404NotFound, "Not found"),
            _ => (0, "")
        };
        if (status == 0) return false;

        http.Response.StatusCode = status;
        return await problems.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = http,
            ProblemDetails = new ProblemDetails { Status = status, Title = title, Detail = exception.Message },
            Exception = exception
        });
    }
}

ER'den EF Core'a ve Geri

ER diagram yazısındaki dönüşüm kuralları HrPortalDbContext'te: composition cascade'li FK (Foreign Key), association Restrict, Approvals üzerinde (LeaveRequestId, Step) unique index, LeaveRequest.Id için 42'den başlayan identity. Şema migration'la oluşuyor (dotnet ef migrations add InitialCreate, dotnet-ef repo'daki local tool manifest'inden), API açılırken MigrateAsync ile uygulanıyor. ER'nin kendisi ise elle değil, aynı modelden üretiliyor; ER yazısındaki MermaidErWriter'ı API project'ine ekleyip küçük bir komut satırı girişi verdik:

// Koddan üretilen diyagram: dotnet run --project src/HrPortal.Api --no-launch-profile -- --er
if (args is ["--er"])
{
    var options = new DbContextOptionsBuilder<HrPortalDbContext>().UseSqlServer().Options;
    using var model = new HrPortalDbContext(options);
    Console.Write(MermaidErWriter.Write(model.Model));
    return;
}

--no-launch-profile bir ayrıntı ama önemli: o olmadan dotnet run standart çıktıya "Using launch settings from ..." satırını basıyor ve bu satır er.mmd dosyasının ilk satırına yazılıp Mermaid'i bozuyor. Üretilen dosyadan bir parça:

erDiagram
    Approvals {
        int Id PK
        int ApproverId FK
        nvarchar(500)? Comment
        datetimeoffset DecidedAt
        nvarchar(20) Decision
        int LeaveRequestId FK, UK
        nvarchar(20) Step UK
    }
    Departments {
        int Id PK
        int? DirectorId FK
        nvarchar(100) Name
    }

C4'ten Çalışan Sisteme ve Angular'a

C4 container diyagramındaki kutuların bu repo'daki karşılığı: Web App kutusu web/hr-portal-web, HrPortal.Api kutusu src/HrPortal.Api, HR Database kutusu SQL Server'daki veritabanı. Worker, Redis ve message broker bu iterasyonda yok; takvim sistemi ICalendarGateway'in log yazan bir implementasyonu. Diyagramın kod karşılığı olmayan kutuları da bir bilgi: C4 hedef mimariyi, repo bugünü gösteriyor ve aradaki fark bir backlog. Program.cs container'ın içini, yani 12. yazıdaki component'lerin DI (Dependency Injection) ile nasıl birbirine bağlandığını anlatıyor:

builder.Services.AddDbContext<HrPortalDbContext>(o =>
    o.UseSqlServer(builder.Configuration.GetConnectionString("HrPortal"),
        sql => sql.MigrationsAssembly("HrPortal.Api")));
builder.Services.AddScoped<ILeaveRequestService, LeaveRequestService>();
builder.Services.AddScoped<IWorkingDayCalculator, WorkingDayCalculator>();
builder.Services.AddScoped<IBalanceService, BalanceService>();
builder.Services.AddScoped<ICalendarGateway, LoggingCalendarGateway>();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<LeaveExceptionHandler>();
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));

API'yi SQL Server'a karşı çalıştırıp CASE'teki senaryoyu baştan sona curl'la oynattım: Ada'nın 21–25 Aralık yıllık izni (#42), yöneticisi Alan ve direktörü Grace. Gerçek çıktıdan durum ve bakiye alanları (JSON'un ilgili kısmı, parantez içleri benim notum):

POST /leave-requests -> 201                  "id":42 ... "status":"Draft"
POST /leave-requests/42/submit -> 200        "workingDays":5,"status":"PendingManagerApproval"
POST /leave-requests/42/approve -> 200       "status":"PendingDirectorApproval" (approver E-0420)
POST /leave-requests/42/approve -> 422       "detail":"E-0420 is not the approver for this step."
POST /leave-requests/42/approve -> 200       "status":"Approved" (approver E-0007)
GET /employees/E-1001/leave-balances -> 200  [{"leaveType":"ANNUAL","year":2026,"entitledDays":14,"usedDays":11,"remainingDays":3}]
POST /leave-requests/42/submit -> 409        "detail":"Cannot submit a request in status Approved."
POST /leave-requests/43/submit -> 200        "workingDays":2,"status":"PendingManagerApproval"
POST /leave-requests/43/approve -> 200       "status":"Approved" (approver E-0420)
POST /leave-requests/44/submit -> 422        "title":"Rule violated: balance-exceeded"

Her satır bir diyagram öğesine karşılık geliyor. 5 iş günü R7 gereği direktör adımına gitti (state machine'deki choice), 2 iş günlük mazeret izni (#43) doğrudan onaylandı ([else] kolu), yanlış onaycı 422 aldı (R6), bakiye 14 − 6 − 5 = 3'e indi (R9), dört günlük yeni talep (#44) R3'e takıldı ve onaylanmış talebi yeniden göndermek state machine'de olmadığı için 409 aldı.

Angular tarafında use case'in actor'ü Çalışan'ın gördüğü ekran var. API sözleşmesi bir service'te, enum'lar string union'larda:

// UML enumeration LeaveStatus -> string union (API enum'ları string olarak döner)
export type LeaveStatus =
  | 'Draft'
  | 'PendingManagerApproval'
  | 'PendingDirectorApproval'
  | 'Approved'
  | 'Rejected'
  | 'Cancelled';

@Injectable({ providedIn: 'root' })
export class LeaveRequestApi {
  private readonly http = inject(HttpClient);
  private readonly baseUrl = '/leave-requests';

  // UC-01: POST /leave-requests
  create(body: CreateLeaveRequest): Observable<LeaveRequestDto> {
    return this.http.post<LeaveRequestDto>(this.baseUrl, body);
  }

  // UC-01: POST /leave-requests/{id}/submit
  submit(id: number): Observable<LeaveRequestDto> {
    return this.http.post<LeaveRequestDto>(`${this.baseUrl}/${id}/submit`, {});
  }
}

Form component'i sequence diagram'ın ilk iki mesajını yapıyor: önce taslak oluştur, sonra gönder. DateRange'in "bitiş başlangıçtan önce olamaz" invariant'ı istemcide bir validator olarak tekrar ediyor; sunucudaki kontrolün yerine değil, kullanıcıya erken geri bildirim için:

// DateRange invariant'ının (End >= Start) istemcideki karşılığı
function dateOrder(group: AbstractControl): ValidationErrors | null {
  const { start, end } = group.value as { start: string; end: string };
  return start && end && end < start ? { dateOrder: true } : null;
}

export class LeaveRequestFormComponent {
  // ...

  // Sequence diagram: create (Draft) -> submit (PendingManagerApproval)
  protected send(): void {
    const v = this.form.getRawValue();
    const body: CreateLeaveRequest = { employeeNumber: 'E-1001', ...v, reason: v.reason || null };
    this.busy.set(true);
    this.error.set(null);
    this.api
      .create(body)
      .pipe(switchMap((draft) => this.api.submit(draft.id)))
      .subscribe({
        next: (r) => {
          this.result.set(r);
          this.busy.set(false);
        },
        error: (e: HttpErrorResponse) => {
          // ProblemDetails: 422 iş kuralı, 409 geçersiz geçiş
          this.error.set(e.error?.detail ?? e.message);
          this.busy.set(false);
        },
      });
  }
}

ng build hatasız tamamlandı (ilk bundle 201 kB). Uygulamayı ng serve ve API'ye proxy ile açıp formdan 27 Kasım için tek günlük yıllık izin gönderdiğimde ekranda talebin numarası, "1 working day(s)" ve "status PendingManagerApproval" göründü; aynı formu ikinci kez gönderdiğimde R4'ün mesajı geldi: "Another active request overlaps these dates." Bu ikinci denemenin bir yan etkisi de oldu: taslak oluşturuldu ama gönderilemedi, yani veritabanında sahipsiz bir Draft kaldı. R1 taslağa izin verdiği için bu bir bug değil, ama "taslaklar ne zaman temizlenir?" sorusu requirement'ta yok; bir sonraki iş birimi toplantısının gündem maddesi.

Repo Kuralları: PR Şablonu ve CI

Diyagramı güncel tutmanın tek güvenilir yolu, güncellemeyi PR'ın bir adımı yapmak. PR şablonu:

## What changed
<!-- Requirement / issue id, e.g. R13 -->

## Model impact
- [ ] Requirements (`docs/requirements.md`) updated
- [ ] Hand-drawn diagrams updated (`docs/diagrams/*.puml`) or "no change" explained below
- [ ] Generated ER diagram refreshed (`dotnet run --project src/HrPortal.Api --no-launch-profile -- --er`)
- [ ] EF Core migration added for schema changes

Diagrams checked: use case / domain model / sequence / state machine / C4

"No change explained below" maddesi bilinçli: bir diyagramın değişmediğini söylemek de bir karar ve reviewer'ın görmesi gerekiyor. Elle çizilen diyagramları şablon hatırlatır, üretilen ER'yi ise CI zorlar. PlantUML yazısındaki workflow'a bir adım ekledik:

      - name: Generated ER diagram must be up to date
        run: |
          dotnet run --project src/HrPortal.Api --no-launch-profile -- --er > docs/diagrams/generated/er.mmd
          git diff --exit-code docs/diagrams/generated/er.mmd

Bu adımı yerelde iki durumda çalıştırdım. Temiz repo'da exit code 0. Model değişip er.mmd eski kaldığında ise git diff --exit-code farkı basıp 1 döndürüyor:

--- a/docs/diagrams/generated/er.mmd
+++ b/docs/diagrams/generated/er.mmd
@@ -24,14 +24,15 @@ erDiagram
     LeaveBalances {
         int Id PK
         int EmployeeId FK, UK
-        int EntitledDays
+        decimal(4,1) EntitledDays

Workflow'u GitHub üzerinde koşturmadım; adımların komutlarını aynı sırayla yerelde çalıştırdım.

Değişiklik Senaryosu: Yarım Gün İzin

R13 geldi. Modelin asıl değeri burada ortaya çıkıyor: koda dokunmadan önce diyagramlara tek tek "sen değişiyor musun?" diye soruyoruz. Use case: yeni bir hedef yok, Çalışan hâlâ izin talep ediyor; UC-01 spesifikasyonuna bir alternatif akış eklenir. State machine: yarım günlük talep de aynı durumlardan geçer. C4: yeni container ya da ilişki yok. Domain model: değişir, WorkingDays artık tam sayı değil ve talebin hangi yarım gün olduğu yeni bir attribute. Sequence: mesajlar aynı ama CountWorkingDays'in imzası değişir. ER: kolon tipleri ve yeni bir kolon. Tahmin ettiğin listeyle karşılaştır; sonra gerçek diff:

 docs/diagrams/domain-model.puml                    |  17 +-
 docs/diagrams/generated/er.mmd                     |   7 +-
 docs/diagrams/submit-leave-request.puml            |   2 +-
 docs/requirements.md                               |   1 +
 .../20261107201226_AddHalfDayLeave.Designer.cs     | 358 +++++++++++++++++++++
 .../Migrations/20261107201226_AddHalfDayLeave.cs   |  90 ++++++
 .../Migrations/HrPortalDbContextModelSnapshot.cs   |  20 +-
 src/HrPortal.Leave/Application/Contracts.cs        |  16 +-
 .../Application/LeaveRequestService.cs             |   6 +-
 src/HrPortal.Leave/Domain/Enums.cs                 |   1 +
 src/HrPortal.Leave/Domain/LeaveBalance.cs          |  10 +-
 src/HrPortal.Leave/Domain/LeaveRequest.cs          |  11 +-
 .../Infrastructure/HrPortalDbContext.cs            |   5 +-
 src/HrPortal.Leave/Infrastructure/Services.cs      |  11 +-
 .../src/app/leave/leave-request-api.ts             |   5 +
 .../src/app/leave/leave-request-form.ts            |   6 +
 16 files changed, 526 insertions(+), 40 deletions(-)

use-cases.puml, leave-request-states.puml ve containers.puml listede yok; tahmin doğrulandı. Domain model'in diff'i:

 class LeaveRequest {
   Period : DateRange
-  WorkingDays : int
+  DayPart : DayPart
+  WorkingDays : decimal
   Reason : string [0..1]
   Status : LeaveStatus
-  + Submit(workingDays : int, now : DateTimeOffset)
+  + Submit(workingDays : decimal, now : DateTimeOffset)
@@
 class LeaveBalance {
   Year : int
-  EntitledDays : int
-  UsedDays : int
-  /RemainingDays : int
+  EntitledDays : decimal
+  UsedDays : decimal
+  /RemainingDays : decimal
 }
@@
+enum DayPart {
+  FullDay
+  Morning
+  Afternoon
+}

Kodda yeni kural constructor'a giriyor, çünkü "yarım gün yalnız tek günlük izinde" bir invariant ve geçersiz bir talep hiç oluşmamalı:

public LeaveRequest(int employeeId, int leaveTypeId, DateRange period, DayPart dayPart, string? reason, DateTimeOffset now)
{
    // R13: yarım gün yalnız tek günlük izinde
    if (dayPart != DayPart.FullDay && period.Start != period.End)
        throw new LeaveRuleViolationException("half-day-single-date", "A half day must start and end on the same date.");
    EmployeeId = employeeId;
    LeaveTypeId = leaveTypeId;
    Period = period;
    DayPart = dayPart;
    // ...
}

Migration'da modelin göremeyeceği bir karar vardı. dotnet ef migrations add AddHalfDayLeave yeni DayPart kolonu için defaultValue: "" üretti. Migration'ı bu haliyle v1 verisinin bir kopyasına uyguladım ve mevcut taleplere baktım:

DayPart Requests
------- --------
 3

(1 rows affected)

Üç eski talebin DayPart'ı boş string. API bunu fark etmedi bile: EF Core'un enum-string converter'ı tanımadığı değeri sessizce enum'ın varsayılanına (FullDay) çevirdi ve JSON'da her şey doğru göründü. Ama raporlama sorgusu yazan biri boş string'le karşılaşacaktı. Veri, modelin söylediği şeyi söylemiyordu. Migration'ı elle düzelttim ve veritabanını v1 yedeğinden geri yükleyip yeniden uyguladım:

migrationBuilder.AddColumn<string>(
    name: "DayPart",
    table: "LeaveRequests",
    type: "nvarchar(10)",
    maxLength: 10,
    nullable: false,
    defaultValue: "FullDay"); // mevcut talepler tam gün
DayPart Requests
------- --------
FullDay 3

Ardından yeni senaryo. İki günlük bir yarım gün talebi constructor'da reddediliyor; 27 Kasım sabahı için yarım gün yıllık izin gönderilip yöneticiden onay alıyor ve bakiye 3'ten 2,5'e iniyor:

POST /leave-requests -> 422                  "detail":"A half day must start and end on the same date."
POST /leave-requests -> 201                  "id":45 ... "dayPart":"Morning","workingDays":0.0,"status":"Draft"
POST /leave-requests/45/submit -> 200        "workingDays":0.5,"status":"PendingManagerApproval"
POST /leave-requests/45/approve -> 200       "status":"Approved" (approver E-0420)
GET /employees/E-1001/leave-balances -> 200  [{"leaveType":"ANNUAL","year":2026,"entitledDays":14.0,"usedDays":11.5,"remainingDays":2.5}]

18-change-impact-tr.png

Bu tabloyu kod yazmadan önce, diyagramlara bakarak çıkarabildik. Değişmeyen üç diyagram, değişikliğin sınırını çiziyor: yeni bir actor, yeni bir durum ya da yeni bir dış sistem yok, yani risk domain ve veri katmanında. Model burada bir dokümantasyon değil, bir değişim analizi aracı olarak çalıştı.

Sık Düşülen Tuzaklar

Diyagramı koddan sonra çizmek. Guard sırası hatası, sequence diagram'ı kodla birlikte okusaydık review'da yakalanırdı. Diyagram, kod yazılmadan önce sorulan sorunun cevabıysa değerlidir; sonradan çizilen diyagram kodun bir kopyasıdır ve kodun hatalarını da kopyalar.

Üretilebilecek diyagramı elle çizmek. ER'yi elle tutsaydık decimal(4,1) değişikliği büyük ihtimalle bir sonraki sprinte kadar diyagrama girmezdi. Koddan üretilebilen her şeyi üret, CI'da kontrol et; elle çizileni karar anlatan diyagramlara sakla.

Diyagram ile kod arasındaki bilinçli farkları yazmamak. 11. yazının state machine'i her Approve okunda [hasEnoughBalance] guard'ı taşıyor; bu implementasyon bakiyeyi yalnız son onayda, düşerken kontrol ediyor (LeaveBalance.Deduct yetersiz bakiyede exception atıyor). Ya kod diyagrama uymalı ya diyagram güncellenmeli; ikisi de değilse fark bir yerde yazılı olmalı.

Migration'ı okumadan uygulamak. defaultValue: "" örneği: model doğruydu, diyagram doğruydu, üretilen kod sessizce veriyi bozdu. Şema değişikliği olan her PR'da migration'ın Up method'u bir reviewer tarafından okunmalı.

Modeli bitmiş bir belge sanmak. C4'teki worker ve broker'ın bu repo'da karşılığı yok, UC-05 ve UC-06 yapılmadı, "sahipsiz taslaklar" sorusu açık. Model, sistemin bugünkü hali ile hedeflenen hali arasındaki farkı gösterdiği sürece yaşar.

Küçük Bir Deneme

★☆☆ Kendi projende bir requirement seç ve bu yazıdaki gibi altı diyagrama tek tek "değişiyor musun?" diye sor. Cevaplarını yazdıktan sonra değişikliği yap ve git diff --stat ile karşılaştır. Kaç diyagramı doğru tahmin ettin?

★★☆ Bu yazıdaki implementasyonu 11. yazının state machine'iyle hizala: yönetici onayında da [hasEnoughBalance] guard'ını uygula ve diyagramdaki her ok için bir unit test yaz (serinin State Machine yazısındaki gibi). Hangi testler mevcut kodda kırmızı?

★★★ UC-05 Adjust Leave Balance use case'ini uçtan uca ekle: Cockburn şablonunda spesifikasyon, domain model'de BalanceAdjustment, sequence diagram, migration, endpoint ve İK uzmanı için Angular ekranı. PR'ı şablondaki kontrol listesiyle aç ve hangi diyagramın neden değişmediğini de yaz.

Bu yazıyla UML ve Modeling serisi bitiyor. On sekiz yazı boyunca bir izin talebini requirement metninden use case'e, class ve object diagram'larına, sequence ve activity diagram'larına, state machine'e, component ve deployment diagram'larına, C4'e, ER'ye ve diagrams-as-code araçlarına taşıdık ve sonunda çalışan bir API ile Angular ekranına bağladık. Buradan sonra iki doğal yol var: requirement'ların kendisini nasıl toplayıp yazacağını anlatan ilerideki Requirements Analysis serisi ve bu diyagramları mimari kararlarla birlikte belgelemeyi anlatan Architecture Decision'lar serisi. Hangisini seçersen seç, bir sonraki diyagramı çizmeden önce sor: bu diyagram hangi soruyu cevaplıyor?