Use a workflow step when checkout must coordinate a new durable side effect. Do not put it in a controller: retries, compensation, and recovery belong beside the existing checkout steps.
Insert a step from a provider
import type { ApplicationService } from "@adonisjs/core/types";
import { PlaceOrderWorkflow } from "@adocommercekit/core/services/main";
import type { WorkflowStep } from "@adocommercekit/core";
import type { PlaceOrderContext } from "@adocommercekit/core/services/main";
const reserveMembershipCredit: WorkflowStep<PlaceOrderContext> = {
name: "reserve_membership_credit",
async execute(context) {
// Use context.trx for database state owned by the checkout transaction.
// Store enough identity in context to compensate safely.
},
async compensate(context) {
// Idempotently release only the credit reserved by execute.
},
};
export default class CheckoutExtensionsProvider {
constructor(private app: ApplicationService) {}
async boot() {
const workflow = await this.app.container.make(PlaceOrderWorkflow);
workflow.insertAfter("reserve_inventory", reserveMembershipCredit);
}
}Use the actual step name exported by the running workflow. Fail at boot if the anchor is absent; silently appending changes ordering guarantees.
Design the compensation first
Before implementing execute, answer:
- What durable resource is acquired?
- What stable identifier proves this step owns it?
- Can
executerun twice safely? - Can
compensaterun when execution stopped halfway? - What happens after process death, when in-memory compensation never ran?
If recovery needs new information, persist it before the external side effect and add a reconciliation path.
Keep transaction boundaries visible
Some workflow steps open or join database transactions. External network calls must not hold database locks longer than necessary. Prefer a prepare/commit protocol: persist intent, perform the remote operation with an idempotency key, then finalize with a guarded update.
Test failure injection immediately before and after the step. Assert the cart state, inventory, payment, new resource, and idempotency record—not merely that an exception was thrown.