MealMVCVSAAugust 2026 · 12 min read

MVC Meal Manager Architecture: VSA, CQRS, and Order Lifecycle Engine

TL;DR

MVC Meal Manager is built on Vertical Slice Architecture with CQRS handlers driving a strict order lifecycle state machine (Draft→Submitted→Confirmed→Delivered→Completed). Minimal API endpoints expose commands and queries, while a Vue 3 frontend powers the order dashboard. Role-based access controls separate Admin, Vendor, and Employee capabilities at every layer.

Meal ordering looks simple on the surface—pick a meal, place an order, get food. But the backend that makes this reliable at scale is anything but trivial. Orders move through a strict lifecycle with validation at every transition, vendors need real-time notifications, and three distinct serving types each impose different business rules. MVC Meal Manager uses Vertical Slice Architecture and CQRS to keep this complexity organized and maintainable.

Vertical Slice Architecture: Feature-First Organization

Instead of spreading meal logic across Controllers, Services, and Repositories folders, VSA organizes code by feature. Every meal-related capability lives inside Features/Meal/, grouped into self-contained slices:

Features/
└── Meal/
    ├── CreateOrder/
    │   ├── CreateMealOrderEndpoint.cs
    │   ├── CreateMealOrderCommand.cs
    │   ├── CreateMealOrderHandler.cs
    │   └── CreateMealOrderValidator.cs
    ├── SubmitOrder/
    ├── ConfirmOrder/
    ├── DeliverOrder/
    ├── CompleteOrder/
    ├── GetOrderById/
    ├── ListOrders/
    ├── MealOrder.cs              (aggregate root)
    ├── MealOrderDto.cs
    └── IMealOrderRepository.cs

Each slice contains its endpoint, command or query, handler, and validator. When you need to change how orders are submitted, you open one folder and everything is there. No hunting across layers. This pattern also makes it trivial to add new features—copy an existing slice, rename the pieces, and wire up the new handler.

CQRS Handlers: Commands and Queries, Separated

CQRS (Command Query Responsibility Segregation) splits write operations (commands) from read operations (queries). In MVC Meal Manager, every state-changing action—creating, submitting, confirming, delivering, completing—is a command handled by a dedicated handler. Reads use query handlers that can optimize for performance independently of the write model.

This separation means commands carry the full validation and business rule enforcement, while queries can bypass those rules and project directly into DTOs optimized for the Vue 3 frontend. The result is a system where write-side correctness never compromises read-side performance, and vice versa.

Order Lifecycle State Machine

The heart of the system is the order lifecycle. Every meal order moves through exactly five states, and transitions are only allowed in one direction:

  • Draft: Order created but not yet submitted. Employees can edit or delete.
  • Submitted: Order sent to the vendor. No longer editable by the employee.
  • Confirmed: Vendor acknowledges and accepts the order. Capacity is reserved.
  • Delivered: Meals physically received. Receiving verification completed.
  • Completed: Order closed. No further changes allowed.

The state machine is enforced with a strict enum and transition validation:

public enum MealOrderStatus
{
    Draft = 1,
    Submitted = 2,
    Confirmed = 3,
    Delivered = 4,
    Completed = 5
}

public static class MealOrderStateMachine
{
    private static readonly Dictionary<MealOrderStatus, MealOrderStatus[]>
        AllowedTransitions = new()
        {
            [MealOrderStatus.Draft] = new[] { MealOrderStatus.Submitted },
            [MealOrderStatus.Submitted] = new[] { MealOrderStatus.Confirmed },
            [MealOrderStatus.Confirmed] = new[] { MealOrderStatus.Delivered },
            [MealOrderStatus.Delivered] = new[] { MealOrderStatus.Completed },
            [MealOrderStatus.Completed] = Array.Empty<MealOrderStatus>()
        };

    public static bool CanTransition(
        MealOrderStatus from, MealOrderStatus to) =>
        AllowedTransitions.TryGetValue(from, out var allowed) &&
        allowed.Contains(to);
}

Every state transition handler checks this validation before persisting changes. If an attempt is made to jump from Draft to Delivered—or, worse, to move a Completed order—the handler throws a domain exception that propagates to the API layer as a 422 Unprocessable Entity.

CQRS State Transition Handler in Action

Here is the SubmitOrder handler showing how business rules, state machine validation, and vendor notification combine in a single slice:

public sealed class SubmitMealOrderHandler(
    IMealOrderRepository repo,
    IServingTypeValidator servingValidator,
    IVendorNotifier notifier)
    : ICommandHandler<SubmitMealOrderCommand, MealOrderDto>
{
    public async Task<MealOrderDto> Handle(
        SubmitMealOrderCommand command, CancellationToken ct)
    {
        var order = await repo.GetByIdAsync(command.OrderId, ct)
            ?? throw new NotFoundException("Order not found");

        if (!MealOrderStateMachine.CanTransition(
            order.Status, MealOrderStatus.Submitted))
            throw new DomainException(
                $"Cannot submit order in {order.Status} status");

        var servingType = await servingValidator
            .GetServingTypeAsync(order.ServingTypeId, ct);

        servingValidator.EnsureCutoffNotPassed(
            servingType, order.OrderDate);

        order.Submit();
        await repo.UpdateAsync(order, ct);

        await notifier.NotifyVendorOrderReceivedAsync(
            order.VendorId, order.Id, ct);

        return MealOrderDto.FromEntity(order);
    }
}

Business Workflow Rules by Serving Type

Each serving type applies different business rules at each state transition. The workflow engine evaluates these rules before allowing a transition:

  • Routine: Orders must be submitted before the daily cutoff (e.g., 10:00 AM). Late submissions are rejected. Confirmation is automatic for recurring vendors with good standing. Delivery is batched by time slot.
  • In Advance: Orders must be placed at least 24 hours before the serving date. Vendor confirmation is required manually. Delivery verification includes headcount matching against the original order.
  • Instant: Orders bypass most scheduling constraints but enforce vendor capacity limits in real time. If a vendor reaches capacity for a time slot, the system rejects further Instant orders for that slot.

Minimal API Endpoints

All meal operations are exposed through Minimal API endpoints mapped in Features/Meal/MealEndpoints.cs. Each endpoint delegates to its CQRS handler and returns standardized API responses:

public static void MapMealEndpoints(this IEndpointRouteBuilder app)
{
    var group = app.MapGroup("/api/meals")
        .RequireAuthorization();

    group.MapPost("/", async (
        CreateMealOrderCommand cmd,
        ICommandHandler<CreateMealOrderCommand, MealOrderDto> handler,
        CancellationToken ct) =>
    {
        var result = await handler.Handle(cmd, ct);
        return Results.Created($"/api/meals/{result.Id}", result);
    }).RequireAuthorization("Employee");

    group.MapPut("/{id:guid}/submit", async (
        Guid id,
        ICommandHandler<SubmitMealOrderCommand, MealOrderDto> handler,
        CancellationToken ct) =>
    {
        var result = await handler.Handle(
            new SubmitMealOrderCommand(id), ct);
        return Results.Ok(result);
    }).RequireAuthorization("Employee");

    group.MapGet("/", async (
        [AsParameters] ListMealOrdersQuery query,
        IQueryHandler<ListMealOrdersQuery, PagedList<MealOrderDto>> handler,
        CancellationToken ct) =>
    {
        var result = await handler.Handle(query, ct);
        return Results.Ok(result);
    });
}

Vue 3 Frontend with Order Dashboard

The frontend is built with Vue 3 and consumes the Minimal API endpoints. The order dashboard provides role-specific views: employees see their own orders with create and submit actions, vendors see incoming orders requiring confirmation, and administrators see the full order pipeline with filtering by status, vendor, and date range.

Vue 3's reactivity system updates the dashboard in real time as orders transition through states. When a vendor confirms an order, the employee's dashboard reflects the change without a page reload. The component tree mirrors the VSA organization, with meal-related components grouped under a Meal/ directory in the frontend as well.

Role-Based Access Across Layers

Three roles control access at every layer—API, handler, and UI:

  • Admin: Full access to all orders, vendors, and reporting. Can override state transitions in exceptional cases.
  • Vendor: Views incoming orders, confirms or rejects, and marks deliveries. Cannot see orders from other vendors.
  • Employee: Creates and submits personal orders, views order history. Cannot modify orders after submission.

Role checks are enforced at the endpoint level via .RequireAuthorization("Vendor"), at the handler level with policy-based guards, and at the UI level where Vue components conditionally render actions based on the current user's claims.

FAQ

How does the order lifecycle work? Orders move through five sequential states: Draft→Submitted→Confirmed→Delivered→Completed. Each transition is validated by a state machine that rejects invalid jumps. Only forward transitions are allowed, and Completed orders are immutable.

What validation happens at each state transition? The state machine checks transition validity, the serving type validator enforces cutoff times and capacity limits, and role-based authorization ensures only the correct role can trigger each transition (employees submit, vendors confirm, receivers verify delivery).

How are vendors notified? The CQRS handler dispatches notifications after successful state transitions via IVendorNotifier. When an order is submitted, the assigned vendor receives a notification. Confirmation and delivery events also trigger notifications to keep all parties in sync.

Want to build your own VSA + CQRS system?

MVC EDevKit Basic provides a clean Vertical Slice Architecture foundation with CQRS handlers, Minimal APIs, and role-based authorization. $21.

View MVC EDevKit Details