diff --git a/Booking/Booking.Api/Booking.Api.csproj b/Booking/Booking.Api/Booking.Api.csproj
new file mode 100644
index 00000000..3cf96a68
--- /dev/null
+++ b/Booking/Booking.Api/Booking.Api.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Booking/Booking.Api/Contracts/CreateAppointmentRequest.cs b/Booking/Booking.Api/Contracts/CreateAppointmentRequest.cs
new file mode 100644
index 00000000..62121fe6
--- /dev/null
+++ b/Booking/Booking.Api/Contracts/CreateAppointmentRequest.cs
@@ -0,0 +1,7 @@
+namespace Booking.Api.Contracts;
+
+public sealed record CreateAppointmentRequest(
+ DateTime StartUtc,
+ DateTime EndUtc,
+ string Customer
+);
\ No newline at end of file
diff --git a/Booking/Booking.Api/Program.cs b/Booking/Booking.Api/Program.cs
new file mode 100644
index 00000000..fab1c69e
--- /dev/null
+++ b/Booking/Booking.Api/Program.cs
@@ -0,0 +1,69 @@
+using Booking.Api.Contracts;
+using Booking.Core;
+using Booking.Infrastructure;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// OpenAPI + Swagger UI
+builder.Services.AddOpenApi();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+// DI
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+var app = builder.Build();
+
+// Swagger UI in Development
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+// -------------------------
+// Endpoints (per spec)
+// -------------------------
+
+app.MapPost("/appointments", async (
+ CreateAppointmentRequest req,
+ BookingService service,
+ CancellationToken ct) =>
+{
+ try
+ {
+ var created = await service.CreateAsync(
+ startUtc: req.StartUtc,
+ endUtc: req.EndUtc,
+ customer: req.Customer,
+ ct: ct);
+
+ return Results.Ok(created);
+ }
+ catch (ArgumentException ex)
+ {
+ return Results.BadRequest(new { error = ex.Message });
+ }
+ catch (InvalidOperationException ex) when (ex.Message == BookingErrors.Conflict)
+ {
+ return Results.Conflict(new { error = ex.Message });
+ }
+})
+.WithName("CreateAppointment")
+.WithOpenApi();
+
+app.MapGet("/appointments", async (
+ IAppointmentRepository repo,
+ CancellationToken ct) =>
+{
+ var items = await repo.GetAllAsync(ct);
+ return Results.Ok(items);
+})
+.WithName("GetAppointments")
+.WithOpenApi();
+
+app.Run();
\ No newline at end of file
diff --git a/Booking/Booking.Core/Appointment.cs b/Booking/Booking.Core/Appointment.cs
new file mode 100644
index 00000000..bac3399f
--- /dev/null
+++ b/Booking/Booking.Core/Appointment.cs
@@ -0,0 +1,8 @@
+namespace Booking.Core;
+
+public sealed record Appointment(
+ Guid Id,
+ DateTime StartUtc,
+ DateTime EndUtc,
+ string Customer
+);
\ No newline at end of file
diff --git a/Booking/Booking.Core/Booking.Core.csproj b/Booking/Booking.Core/Booking.Core.csproj
new file mode 100644
index 00000000..b7601447
--- /dev/null
+++ b/Booking/Booking.Core/Booking.Core.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/Booking/Booking.Core/BookingErrors.cs b/Booking/Booking.Core/BookingErrors.cs
new file mode 100644
index 00000000..2ce34b85
--- /dev/null
+++ b/Booking/Booking.Core/BookingErrors.cs
@@ -0,0 +1,10 @@
+namespace Booking.Core;
+
+public static class BookingErrors
+{
+ public const string CustomerRequired = "Customer is required.";
+ public const string InvalidRange = "EndUtc must be after StartUtc.";
+ public const string TooShort = "Minimum duration is 15 minutes.";
+ public const string InPast = "Cannot book an appointment in the past.";
+ public const string Conflict = "Appointment time conflicts with an existing booking.";
+}
\ No newline at end of file
diff --git a/Booking/Booking.Core/BookingService.cs b/Booking/Booking.Core/BookingService.cs
new file mode 100644
index 00000000..81d8b756
--- /dev/null
+++ b/Booking/Booking.Core/BookingService.cs
@@ -0,0 +1,50 @@
+namespace Booking.Core;
+
+public sealed class BookingService
+{
+ private readonly IAppointmentRepository _repo;
+ private readonly ITimeProvider _time;
+
+ public BookingService(IAppointmentRepository repo, ITimeProvider time)
+ {
+ _repo = repo;
+ _time = time;
+ }
+
+ public async Task CreateAsync(
+ DateTime startUtc,
+ DateTime endUtc,
+ string customer,
+ CancellationToken ct = default)
+ {
+ if (string.IsNullOrWhiteSpace(customer))
+ throw new ArgumentException(BookingErrors.CustomerRequired);
+
+ // (Spec) End must be after Start
+ if (endUtc <= startUtc)
+ throw new ArgumentException(BookingErrors.InvalidRange);
+
+ // (Spec) Min duration 15 minutes
+ if (endUtc - startUtc < TimeSpan.FromMinutes(15))
+ throw new ArgumentException(BookingErrors.TooShort);
+
+ // (Spec) Cannot book in the past
+ if (startUtc < _time.UtcNow)
+ throw new ArgumentException(BookingErrors.InPast);
+
+ // (Spec) No overlap allowed
+ var conflict = await _repo.ExistsOverlapAsync(startUtc, endUtc, ct);
+ if (conflict)
+ throw new InvalidOperationException(BookingErrors.Conflict);
+
+ var appointment = new Appointment(
+ Id: Guid.NewGuid(),
+ StartUtc: startUtc,
+ EndUtc: endUtc,
+ Customer: customer.Trim()
+ );
+
+ await _repo.AddAsync(appointment, ct);
+ return appointment;
+ }
+}
\ No newline at end of file
diff --git a/Booking/Booking.Core/IAppointmentRepository.cs b/Booking/Booking.Core/IAppointmentRepository.cs
new file mode 100644
index 00000000..36c93a4a
--- /dev/null
+++ b/Booking/Booking.Core/IAppointmentRepository.cs
@@ -0,0 +1,8 @@
+namespace Booking.Core;
+
+public interface IAppointmentRepository
+{
+ Task AddAsync(Appointment appointment, CancellationToken ct = default);
+ Task> GetAllAsync(CancellationToken ct = default);
+ Task ExistsOverlapAsync(DateTime startUtc, DateTime endUtc, CancellationToken ct = default);
+}
\ No newline at end of file
diff --git a/Booking/Booking.Core/TimeProvider.cs b/Booking/Booking.Core/TimeProvider.cs
new file mode 100644
index 00000000..5f61436e
--- /dev/null
+++ b/Booking/Booking.Core/TimeProvider.cs
@@ -0,0 +1,11 @@
+namespace Booking.Core;
+
+public interface ITimeProvider
+{
+ DateTime UtcNow { get; }
+}
+
+public sealed class SystemTimeProvider : ITimeProvider
+{
+ public DateTime UtcNow => DateTime.UtcNow;
+}
\ No newline at end of file
diff --git a/Booking/Booking.Infrastructure/Booking.Infrastructure.csproj b/Booking/Booking.Infrastructure/Booking.Infrastructure.csproj
new file mode 100644
index 00000000..eaba4021
--- /dev/null
+++ b/Booking/Booking.Infrastructure/Booking.Infrastructure.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Booking/Booking.Infrastructure/InMemoryAppointmentRepository.cs b/Booking/Booking.Infrastructure/InMemoryAppointmentRepository.cs
new file mode 100644
index 00000000..c292374c
--- /dev/null
+++ b/Booking/Booking.Infrastructure/InMemoryAppointmentRepository.cs
@@ -0,0 +1,37 @@
+using Booking.Core;
+
+namespace Booking.Infrastructure;
+
+public sealed class InMemoryAppointmentRepository : IAppointmentRepository
+{
+ private readonly List _items = new();
+ private readonly object _lock = new();
+
+ public Task AddAsync(Appointment appointment, CancellationToken ct = default)
+ {
+ lock (_lock)
+ {
+ _items.Add(appointment);
+ }
+ return Task.CompletedTask;
+ }
+
+ public Task> GetAllAsync(CancellationToken ct = default)
+ {
+ lock (_lock)
+ {
+ return Task.FromResult((IReadOnlyList)_items.ToList());
+ }
+ }
+
+ public Task ExistsOverlapAsync(DateTime startUtc, DateTime endUtc, CancellationToken ct = default)
+ {
+ lock (_lock)
+ {
+ // Spec overlap rule:
+ // overlap when newStart < existingEnd AND newEnd > existingStart
+ var exists = _items.Any(a => startUtc < a.EndUtc && endUtc > a.StartUtc);
+ return Task.FromResult(exists);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Booking/Booking.Tests/Booking.Tests.csproj b/Booking/Booking.Tests/Booking.Tests.csproj
new file mode 100644
index 00000000..86519048
--- /dev/null
+++ b/Booking/Booking.Tests/Booking.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Booking/Booking.Tests/BookingServiceTests.cs b/Booking/Booking.Tests/BookingServiceTests.cs
new file mode 100644
index 00000000..5deeff87
--- /dev/null
+++ b/Booking/Booking.Tests/BookingServiceTests.cs
@@ -0,0 +1,138 @@
+using Booking.Core;
+using Booking.Infrastructure;
+using FluentAssertions;
+
+namespace Booking.Tests;
+
+public class BookingServiceTests
+{
+ private static (BookingService service, InMemoryAppointmentRepository repo, FakeTimeProvider time) CreateSut(DateTime nowUtc)
+ {
+ var repo = new InMemoryAppointmentRepository();
+ var time = new FakeTimeProvider { UtcNow = nowUtc };
+ var service = new BookingService(repo, time);
+ return (service, repo, time);
+ }
+
+ [Fact]
+ public async Task CreateAsync_Should_Create_Appointment_When_Valid()
+ {
+ var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc);
+ var (service, repo, _) = CreateSut(now);
+
+ var start = now.AddHours(2);
+ var end = start.AddMinutes(30);
+
+ var appt = await service.CreateAsync(start, end, "John");
+
+ appt.Id.Should().NotBeEmpty();
+ appt.Customer.Should().Be("John");
+ appt.StartUtc.Should().Be(start);
+ appt.EndUtc.Should().Be(end);
+
+ var all = await repo.GetAllAsync();
+ all.Should().HaveCount(1);
+ }
+
+ [Fact]
+ public async Task CreateAsync_Should_Reject_Empty_Customer()
+ {
+ var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc);
+ var (service, _, _) = CreateSut(now);
+
+ var start = now.AddHours(1);
+ var end = start.AddMinutes(30);
+
+ Func act = () => service.CreateAsync(start, end, " ");
+
+ await act.Should().ThrowAsync()
+ .WithMessage(BookingErrors.CustomerRequired);
+ }
+
+ [Fact]
+ public async Task CreateAsync_Should_Reject_Invalid_Range_When_End_Before_Or_Equal_Start()
+ {
+ var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc);
+ var (service, _, _) = CreateSut(now);
+
+ var start = now.AddHours(1);
+ var end = start; // equal
+
+ Func act = () => service.CreateAsync(start, end, "A");
+
+ await act.Should().ThrowAsync()
+ .WithMessage(BookingErrors.InvalidRange);
+ }
+
+ [Fact]
+ public async Task CreateAsync_Should_Reject_Too_Short_Duration()
+ {
+ var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc);
+ var (service, _, _) = CreateSut(now);
+
+ var start = now.AddHours(1);
+ var end = start.AddMinutes(10);
+
+ Func act = () => service.CreateAsync(start, end, "A");
+
+ await act.Should().ThrowAsync()
+ .WithMessage(BookingErrors.TooShort);
+ }
+
+ [Fact]
+ public async Task CreateAsync_Should_Reject_Start_In_Past()
+ {
+ var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc);
+ var (service, _, _) = CreateSut(now);
+
+ var start = now.AddMinutes(-1);
+ var end = now.AddMinutes(30);
+
+ Func act = () => service.CreateAsync(start, end, "A");
+
+ await act.Should().ThrowAsync()
+ .WithMessage(BookingErrors.InPast);
+ }
+
+ [Fact]
+ public async Task CreateAsync_Should_Reject_Overlap()
+ {
+ var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc);
+ var (service, _, _) = CreateSut(now);
+
+ var start1 = now.AddHours(1);
+ var end1 = start1.AddMinutes(30);
+
+ await service.CreateAsync(start1, end1, "A");
+
+ // overlaps: starts inside existing
+ Func act = () => service.CreateAsync(
+ start1.AddMinutes(10),
+ end1.AddMinutes(10),
+ "B");
+
+ await act.Should().ThrowAsync()
+ .WithMessage(BookingErrors.Conflict);
+ }
+
+ [Fact]
+ public async Task CreateAsync_Should_Allow_Boundary_Touching_End_Equals_Next_Start()
+ {
+ var now = new DateTime(2026, 02, 25, 12, 00, 00, DateTimeKind.Utc);
+ var (service, repo, _) = CreateSut(now);
+
+ var start1 = now.AddHours(1);
+ var end1 = start1.AddMinutes(30);
+
+ await service.CreateAsync(start1, end1, "A");
+
+ // boundary touch: start == existing end => NOT overlap per spec
+ var start2 = end1;
+ var end2 = start2.AddMinutes(30);
+
+ var appt2 = await service.CreateAsync(start2, end2, "B");
+
+ appt2.Customer.Should().Be("B");
+ (await repo.GetAllAsync()).Should().HaveCount(2);
+ }
+}
\ No newline at end of file
diff --git a/Booking/Booking.Tests/FakeTimeProvider.cs b/Booking/Booking.Tests/FakeTimeProvider.cs
new file mode 100644
index 00000000..df2fdc09
--- /dev/null
+++ b/Booking/Booking.Tests/FakeTimeProvider.cs
@@ -0,0 +1,8 @@
+using Booking.Core;
+
+namespace Booking.Tests;
+
+public sealed class FakeTimeProvider : ITimeProvider
+{
+ public DateTime UtcNow { get; set; }
+}
\ No newline at end of file
diff --git a/Booking/README.md b/Booking/README.md
new file mode 100644
index 00000000..bb37e498
--- /dev/null
+++ b/Booking/README.md
@@ -0,0 +1,74 @@
+# Booking API (Spec-Driven + Codegen + Tests)
+
+A small appointment booking API built using a spec-driven workflow and code generation tools.
+
+## What it does (workflow)
+
+- Create an appointment (POST /appointments)
+- View appointments (GET /appointments)
+- Enforces domain rules:
+ - EndUtc must be after StartUtc
+ - Minimum duration is 15 minutes
+ - Cannot book in the past
+ - No overlapping appointments
+
+Data is stored in-memory (no database required).
+
+## Spec and codegen evidence
+
+- Spec: `Booking/booking.spec.md`
+- Codegen log: `Booking/docs/codegen-log.md`
+
+## Tech
+
+- .NET 10 Minimal API
+- xUnit + FluentAssertions
+- Swagger UI
+
+## Run the API locally
+
+From the repo root:
+
+```bash
+dotnet run --project Booking/Booking.Api
+```
+
+Open Swagger UI:
+
+```
+https://localhost:xxxx/swagger
+```
+
+(The exact port is printed in the console)
+
+## Example requests
+
+### Create appointment
+
+POST /appointments
+
+```json
+{
+ "startUtc": "2026-03-01T18:00:00Z",
+ "endUtc": "2026-03-01T18:30:00Z",
+ "customer": "John"
+}
+```
+
+### List appointments
+
+GET /appointments
+
+## Run tests
+
+From the repo root:
+
+```bash
+dotnet test
+```
+
+## Notes
+
+- No secrets/credentials are used or required.
+- Business rules are implemented in Booking.Core.
+- Data storage is in-memory for simplicity.
\ No newline at end of file
diff --git a/Booking/booking.spec.md b/Booking/booking.spec.md
new file mode 100644
index 00000000..15f1d3c3
--- /dev/null
+++ b/Booking/booking.spec.md
@@ -0,0 +1,36 @@
+# Booking Spec (Spec-Driven)
+
+## Feature: Appointment booking
+
+### Domain rules
+1) Appointment has StartUtc and EndUtc (UTC).
+2) EndUtc must be after StartUtc.
+3) Minimum duration: 15 minutes.
+4) Cannot book in the past: StartUtc < UtcNow is invalid.
+5) No overlaps allowed:
+ Overlap exists when: newStart < existingEnd AND newEnd > existingStart.
+
+### Data model
+Appointment:
+- id: guid
+- startUtc: datetime (UTC)
+- endUtc: datetime (UTC)
+- customer: string
+
+### API
+POST /appointments
+Request JSON:
+{
+ "startUtc": "2026-02-24T18:00:00Z",
+ "endUtc": "2026-02-24T18:30:00Z",
+ "customer": "John"
+}
+
+Responses:
+- 200 OK: returns created Appointment
+- 400 BadRequest: validation errors (range, duration, past, empty customer)
+- 409 Conflict: overlaps an existing appointment
+
+GET /appointments
+Responses:
+- 200 OK: returns list of Appointment
\ No newline at end of file
diff --git a/Booking/codegen-log.md b/Booking/codegen-log.md
new file mode 100644
index 00000000..1f19c5e3
--- /dev/null
+++ b/Booking/codegen-log.md
@@ -0,0 +1,45 @@
+# Codegen Log (Evidence)
+
+## Tools used
+- Visual Studio 2026
+- GitHub Copilot (and/or ChatGPT GPT-5.2 / Codex)
+
+## Workflow
+1) Wrote the spec first: booking.spec.md
+2) Used codegen to scaffold: domain model, repo interface, service, API endpoints, tests
+3) Reviewed and refined generated code to match the spec exactly
+
+## Prompts (copy/paste)
+### P1 - Domain model + repo interface
+"Generate C# code for Booking.Core:
+- Appointment record (Id, StartUtc, EndUtc, Customer)
+- IAppointmentRepository with AddAsync, GetAllAsync, ExistsOverlapAsync
+based on booking.spec.md"
+
+### P2 - Domain service
+"Generate BookingService enforcing spec rules:
+- end after start
+- min 15 min
+- not in past using injectable time provider
+- no overlap using repository
+Throw ArgumentException for validation and InvalidOperationException for conflict"
+
+### P3 - In-memory repo
+"Generate InMemoryAppointmentRepository implementing IAppointmentRepository with overlap check:
+newStart < existingEnd && newEnd > existingStart
+Use thread-safety."
+
+### P4 - Minimal API endpoints
+"Generate Minimal API endpoints in Booking.Api Program.cs:
+POST /appointments and GET /appointments
+Return 400 for validation, 409 for overlap, 200 for success.
+Include Swagger."
+
+### P5 - Tests from spec
+"Generate xUnit tests using FluentAssertions:
+- valid booking
+- past booking rejected
+- invalid range rejected
+- too short rejected
+- overlap rejected
+- boundary touching allowed (end == next start)"
\ No newline at end of file