Tutorial

11.3 Fulfilment is a train

Packing happens in the warehouse, which, like the payment provider and the supplier, is somebody else's system behind an interface.

Shopfront.Core/Warehouse/IWarehouse.cs:

namespace Shopfront.Warehouse;
 
public interface IWarehouse
{
    // Packs the order's items and hands back the parcel's tracking number.
    Task<string> Pack(Guid orderId, int items, CancellationToken cancellationToken);
}

Shopfront.Core/Warehouse/FakeWarehouse.cs:

namespace Shopfront.Warehouse;
 
// Takes ten seconds to pack anything, long enough to kill the worker while it does.
public class FakeWarehouse : IWarehouse
{
    public TimeSpan PackingTime { get; init; } = TimeSpan.FromSeconds(10);
 
    public async Task<string> Pack(Guid orderId, int items, CancellationToken cancellationToken)
    {
        await Task.Delay(PackingTime, cancellationToken);
        return $"PKG-{orderId.ToString("N")[..8].ToUpperInvariant()}";
    }
}

The train takes what packing needs: the order and how many items are in it. It carries no [TraxAuthorize]: it is queued by PlaceOrder, not by anyone outside.

Shopfront.Core/Trains/FulfilOrder/FulfilOrderInput.cs:

namespace Shopfront.Trains.FulfilOrder;
 
public record FulfilOrderInput
{
    public required Guid OrderId { get; init; }
    public required int Items { get; init; }
}

Shopfront.Core/Trains/FulfilOrder/FulfilOrderOutput.cs:

namespace Shopfront.Trains.FulfilOrder;
 
public record FulfilOrderOutput(Guid OrderId, string TrackingNumber);

Shopfront.Core/Trains/FulfilOrder/IFulfilOrderTrain.cs:

using Trax.Effect.Services.ServiceTrain;
 
namespace Shopfront.Trains.FulfilOrder;
 
public interface IFulfilOrderTrain : IServiceTrain<FulfilOrderInput, FulfilOrderOutput>;

Shopfront.Core/Trains/FulfilOrder/Junctions/PackOrderJunction.cs:

using Shopfront.Warehouse;
using Trax.Effect.Services.EffectJunction;
 
namespace Shopfront.Trains.FulfilOrder.Junctions;
 
internal class PackOrderJunction(IWarehouse warehouse, ILogger<PackOrderJunction> logger)
    : EffectJunction<FulfilOrderInput, FulfilOrderOutput>
{
    public override async Task<FulfilOrderOutput> Run(FulfilOrderInput input)
    {
        logger.LogInformation("Packing order {OrderId}", input.OrderId);
        var tracking = await warehouse.Pack(input.OrderId, input.Items, CancellationToken);
        logger.LogInformation("Packed order {OrderId} as {Tracking}", input.OrderId, tracking);
        return new FulfilOrderOutput(input.OrderId, tracking);
    }
}

Shopfront.Core/Trains/FulfilOrder/FulfilOrderTrain.cs:

using Shopfront.Trains.FulfilOrder.Junctions;
using Trax.Core.Functional;
using Trax.Effect.Models.Metadata;
using Trax.Effect.Services.ServiceTrain;
 
namespace Shopfront.Trains.FulfilOrder;
 
public class FulfilOrderTrain : ServiceTrain<FulfilOrderInput, FulfilOrderOutput>, IFulfilOrderTrain
{
    protected override Task<Either<Exception, FulfilOrderOutput>> Junctions() =>
        Chain<PackOrderJunction>().Resolve();
 
    // One order is fulfilled one queued run at a time, however often it is queued.
    protected override string? QueueSubjectKey(Metadata metadata) =>
        $"order:{TrainInput.OrderId}";
}

QueueSubjectKey names what the queued work is about. Two queued runs with the same key never run at once: the second waits until the first is done. An order queued twice by mistake is therefore never packed by two workers side by side.

SDK Reference

AddTraxWorker | ITrainExecutionService | Queued work | Job submission | Structuring your application | Host tracking


Next: 11.4 PlaceOrder queues it