BookingMVCVSAAugust 2026 · 11 min read

MVC Booking Manager Architecture: VSA, State Machine, and Resource Validation

TL;DR

MVC Booking Manager is built on Vertical Slice Architecture with CQRS handlers for every booking operation. A strict six-state state machine guards every transition, resource availability is validated with overlap queries, and check-in/check-out flows capture condition snapshots for accountability. The frontend uses Vue 3 with a calendar-based booking interface backed by Minimal API endpoints.

Behind the booking calendar and resource catalog lies an architecture that enforces business rules at the domain level, not just in the UI. MVC Booking Manager uses Vertical Slice Architecture (VSA) to keep each booking feature self-contained, a state machine to prevent invalid transitions, and validation layers that ensure no two bookings can claim the same resource at the same time. This article walks through the architecture layer by layer.

Vertical Slice Architecture: Features Over Layers

Instead of organizing code by technical layer — Controllers, Services, Repositories — the Booking Manager uses VSA. Every booking feature lives in its own folder under Features/Booking/. A feature folder contains everything that feature needs: the command or query, its handler, any validators, the API endpoint registration, and the response DTO. There is no shared service layer that every feature depends on; each slice owns its data access and business logic.

This structure means you can read a single folder and understand exactly how booking creation works — the input model, the validation rules, the handler logic, and the endpoint. When you need to modify approval workflows, you open Features/Booking/Approve/ and everything is right there. No hunting across half a dozen projects or layers.

CQRS Handlers for Booking Operations

Every booking operation is a command or query handled by a dedicated handler. CreateBooking, ApproveBooking, AssignResource, CheckIn, CheckOut, CancelBooking — each is its own handler class. This aligns perfectly with the state machine: each handler corresponds to a state transition and enforces only the rules relevant to that transition.

Handlers use MediatR for dispatch, which also enables cross-cutting concerns like validation, logging, and transaction management through pipeline behaviors. A single booking operation flows through validation behavior, then the handler, then any post-processing — all without the handler itself knowing about the pipeline.

State Machine: Six States with Transition Guards

The booking state machine is the architectural centerpiece. Six states — Draft, Submitted, Approved, Assigned, InUse, Completed — define where a booking is, and transition methods define where it can go. Each transition is a method on the Booking entity that checks preconditions before changing state:

public BookingTransitionResult Submit()
{
    if (Status != BookingStatus.Draft)
        return BookingTransitionResult.Invalid("Only draft bookings can be submitted.");

    if (string.IsNullOrWhiteSpace(Purpose))
        return BookingTransitionResult.Invalid("A purpose is required before submission.");

    Status = BookingStatus.Submitted;
    SubmittedAt = DateTimeOffset.UtcNow;
    return BookingTransitionResult.Success();
}

public BookingTransitionResult Approve(string approverId)
{
    if (Status != BookingStatus.Submitted)
        return BookingTransitionResult.Invalid("Only submitted bookings can be approved.");

    Status = BookingStatus.Approved;
    ApprovedBy = approverId;
    ApprovedAt = DateTimeOffset.UtcNow;
    return BookingTransitionResult.Success();
}

public BookingTransitionResult Assign(string resourceId)
{
    if (Status != BookingStatus.Approved)
        return BookingTransitionResult.Invalid("Only approved bookings can be assigned a resource.");

    Status = BookingStatus.Assigned;
    ResourceId = resourceId;
    AssignedAt = DateTimeOffset.UtcNow;
    return BookingTransitionResult.Success();
}

These guards live on the domain entity, not in controllers or services. No matter how the system is accessed — MVC controller, Minimal API, background job, or even a future mobile API — the same rules apply. A booking can never skip from Draft to Assigned, and an attempt to do so returns a clear reason why it failed.

Resource Availability Validation

Double-booking is the cardinal sin of resource management. The availability check runs an overlap query that considers all active and upcoming bookings for the target resource. The core logic is simple but precise:

public async Task<bool> IsResourceAvailableAsync(
    string resourceId, DateTimeOffset start, DateTimeOffset end,
    string? excludeBookingId = null)
{
    var query = _db.Bookings
        .Where(b => b.ResourceId == resourceId)
        .Where(b => b.Status != BookingStatus.Draft)
        .Where(b => b.Status != BookingStatus.Completed)
        .Where(b => b.StartTime < end && b.EndTime > start);

    if (excludeBookingId != null)
        query = query.Where(b => b.Id != excludeBookingId);

    return !await query.AnyAsync();
}

The overlap condition StartTime < end AND EndTime > start catches every possible collision: a booking that starts before and ends during, one that starts during and ends after, one that entirely contains the requested window, and one that sits entirely inside it. The exclusion parameter allows re-assignment without the booking colliding with itself.

Check-In/Check-Out with Condition Tracking

The check-in and check-out process goes beyond a simple timestamp. When a user checks out a resource, the system optionally captures condition evidence — photos, notes, odometer readings for vehicles — and stores them alongside the booking. On check-in, the same fields are captured again, and the system compares the two snapshots:

public async Task<CheckInResult> CheckInAsync(CheckInCommand cmd)
{
    var booking = await _db.Bookings
        .Include(b => b.ConditionSnapshots)
        .FirstOrDefaultAsync(b => b.Id == cmd.BookingId);

    if (booking is null)
        return CheckInResult.NotFound();
    if (booking.Status != BookingStatus.InUse)
        return CheckInResult.InvalidState("Only bookings in use can be checked in.");

    var snapshot = new ConditionSnapshot
    {
        BookingId = booking.Id,
        CapturedAt = DateTimeOffset.UtcNow,
        Notes = cmd.ConditionNotes,
        PhotoUrls = cmd.PhotoUrls,
        OdometerReading = cmd.OdometerReading,
        SnapshotType = SnapshotType.CheckIn
    };

    _db.ConditionSnapshots.Add(snapshot);

    var checkoutSnapshot = booking.ConditionSnapshots
        .FirstOrDefault(s => s.SnapshotType == SnapshotType.CheckOut);

    var hasDamage = checkoutSnapshot is not null
        && cmd.HasConditionChanged;

    booking.CheckIn(hasDamage);

    await _db.SaveChangesAsync();
    return CheckInResult.Success(booking, hasDamage);
}

If the check-in condition differs from the check-out condition, the booking is flagged and the resource can be marked for maintenance review. This creates an audit trail that eliminates finger-pointing when equipment is returned damaged.

Minimal API Endpoints

Booking operations are exposed as Minimal API endpoints registered per feature. Each feature folder contains an Endpoints.cs file that maps routes to handlers using the MapGroup pattern. This keeps route definitions colocated with their handlers and avoids a monolithic endpoint registration file. Endpoints use .RequireAuthorization() with role-based policies so that only authorized users can create, approve, or manage bookings.

Vue 3 Calendar-Based Booking Interface

The frontend uses Vue 3 with a calendar component that visualizes resource availability in real time. Users see a timeline view with color-coded blocks representing existing bookings, available slots, and their own pending requests. Clicking an available slot opens a booking creation form pre-filled with the time window. The calendar refreshes via API calls after every booking operation, ensuring all users see the same up-to-date availability.

The Vue 3 components are organized by feature as well — BookingCalendar, BookingForm, ApprovalQueue, ResourceList — each fetching its own data from the Minimal API. This keeps the frontend modular and aligned with the backend VSA structure.

Key Takeaways

  • VSA keeps every booking feature self-contained in its own folder with command, handler, validator, and endpoint
  • The state machine enforces valid transitions at the domain level — no UI bypass possible
  • Overlap queries prevent double-booking with a single, precise condition that catches every collision pattern
  • Check-in/check-out captures condition snapshots that create an immutable audit trail for every resource
  • Vue 3 calendar interface with real-time availability gives users a visual, intuitive booking experience

FAQ

How does the state machine prevent invalid transitions? Every transition method checks the current state before allowing a change. A booking in Draft status cannot be checked in, and a completed booking cannot be cancelled. The guards return descriptive error results that propagate back to the caller, so both the API and the UI can surface clear messages.

How is resource availability checked? An overlap query checks all active and upcoming bookings for the target resource, excluding drafts and completed bookings. The condition StartTime < end AND EndTime > start catches every possible overlap scenario, and the query runs inside the same transaction as the booking creation to prevent race conditions.

What happens during check-in/check-out? Check-out captures a pre-use condition snapshot with optional photos and notes. Check-in captures a post-use snapshot and compares it against the check-out state. Any discrepancy flags the booking for review and can mark the resource as needing maintenance before the next booking.

Want to see this architecture in a complete, production-ready codebase?

MVC EDevKit Basic includes the full booking manager with VSA, state machine, resource validation, and Vue 3 frontend. $21.

View MVC EDevKit Details