Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Batch Operations & Sagas

The generated REST routes work on one row at a time. When the work fans out — inserting hundreds of rows from a CSV, fulfilling an order across inventory, payment, and shipping — two patterns appear: batch for atomic same-table work, and saga for multi-step workflows that span services.

Batch inside a single transaction

The simplest “bulk write” is a single transaction that issues many inserts or updates. Use an action endpoint so the request does not pass through the per-row Validate/Service hooks:

server.Action(maniflex.ActionConfig{
    Method:  "POST",
    Path:    "/users/import",
    Handler: importUsers,
})

func importUsers(ctx *maniflex.ServerContext) error {
    var req struct {
        Users []map[string]any `json:"users"`
    }
    if err := ctx.BindJSON(&req); err != nil {
        return nil
    }

    tx, err := ctx.BeginTx(ctx.Ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()
    ctx.Tx = tx

    users := ctx.GetModel("User")
    inserted := 0
    for _, row := range req.Users {
        if _, err := users.Create(row); err != nil {
            ctx.Abort(http.StatusConflict, "IMPORT_FAILED",
                fmt.Sprintf("row %d: %s", inserted, err.Error()))
            return nil
        }
        inserted++
    }

    if err := tx.Commit(); err != nil {
        return err
    }

    ctx.Response = &maniflex.APIResponse{
        StatusCode: http.StatusCreated,
        Data:       map[string]any{"inserted": inserted},
    }
    return nil
}

Either every row commits or none does. Validation still runs because ctx.GetModel(...).Create goes through the adapter — but per-row middleware on the Service or Validate steps does not, since this is an action.

For larger imports, batch the inserts (INSERT … VALUES (…), (…), … via ctx.RawExec) and commit every N rows.

Cross-service workflows: sagas

When a workflow touches more than one downstream — charge a payment provider, reserve inventory, notify a partner — a single database transaction is no longer enough. The standard pattern is a saga: a sequence of forward steps, each with a compensating undo step.

maniflex does not impose a saga framework. The mechanics fit naturally on the pipeline:

  1. Start a request transaction with maniflex.WithTransaction. The local database changes commit or roll back atomically.
  2. Make external calls from the Service step, recording an outbox row in the same transaction for each call that needs a follow-up.
  3. Process the outbox asynchronously with a background runner (from jobs/redis or a similar) that performs the external call, marks the outbox row done, and triggers compensation on failure.
server.Pipeline.Service.Register(maniflex.WithTransaction(nil),
    maniflex.ForModel("Order"), maniflex.ForOperation(maniflex.OpCreate))

server.Pipeline.Service.Register(func(ctx *maniflex.ServerContext, next func() error) error {
    if err := next(); err != nil {
        return err
    }
    // Both writes are in the same transaction as the Order insert.
    _, err := ctx.GetModel("OutboxEvent").Create(map[string]any{
        "kind":     "charge-payment",
        "payload":  ctx.DBResult,
        "status":   "pending",
    })
    return err
}, maniflex.ForModel("Order"), maniflex.ForOperation(maniflex.OpCreate), maniflex.AtPosition(maniflex.After))

A separate worker reads pending OutboxEvent rows and processes them. If the payment provider fails, the worker records the failure and enqueues a compensating action (e.g. “cancel the order”).

This pattern — transactional outbox + asynchronous worker — is the practical alternative to two-phase commit. It costs you one table and one background job but gains durability and isolation that distributed transactions cannot offer.

When to use which

WorkloadPattern
Bulk same-table writesOne transaction, one action endpoint
Multi-table writes touching only your databasemaniflex.WithTransaction on the request
Writes that depend on external systemsTransactional outbox + saga
Long-running background workBackground job (see Events & Background Jobs)

See also