> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revoengine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 2. Reserve an order

> Implement the reservation Component and its HTTP Endpoint.

Requires the Tables and command validator from step 1.

## Implement the reservation Component

Create a `CODE_TS` Component named `Reserve order balance`. The code below uses the same public declarations delivered to Monaco.

```ts theme={null}
type Order = {
  orderId: string;
  accountId: string;
  amountMinor: string;
  currency: string;
  status: string;
};

type Balance = {
  balanceId: string;
  accountId: string;
  currency: string;
  availableMinor: string;
  reservedMinor: string;
};

const { orderId, requestKey } = api.input()?.body as {
  orderId: string;
  requestKey: string;
};

const operationId = api.getOperationId();
const principal = api.currentUser();
const gateKey = `order-reservation:${requestKey}`;
const settleOrderTemplateId = String(
  api.input()?.templateInputs?.settleOrderTemplateId,
);

if (!util.isUUID(settleOrderTemplateId)) {
  api.throw(500, {
    message: 'Settlement Job template is not configured',
    operationId,
  });
}

const acquired = await api.acquireIdempotencyKey(gateKey, 86_400, {
  operationId,
  orderId,
  state: 'PROCESSING',
});

if (!acquired) {
  const existing = await api.getDatabaseData('transactions', {
    filter: { field: 'requestKey', op: 'eq', value: requestKey },
    take: 1,
  });

  if (existing.results === 1) {
    return {
      operationId,
      originalOperationId: existing.data[0].operationId,
      duplicate: true,
      transactionId: existing.data[0].transactionId,
      status: existing.data[0].status,
    };
  }

  api.throw(409, {
    message:
      'A request with this idempotency key is already processing',
    operationId,
  });
}

let reservation;

try {
  reservation = await api.transactionDatabase(
    async (tx) => {
      const existingTransactions = await tx.getDatabaseData(
        'transactions',
        {
          filter: { field: 'requestKey', op: 'eq', value: requestKey },
          take: 1,
        },
        { lock: 'update' },
      );

      if (existingTransactions.results === 1) {
        return {
          duplicate: true,
          transactionId: existingTransactions.data[0].transactionId,
          originalOperationId: existingTransactions.data[0].operationId,
        };
      }

      const orders = await tx.getDatabaseData<Order>(
        'orders',
        {
          filter: { field: 'orderId', op: 'eq', value: orderId },
          take: 1,
        },
        { lock: 'update' },
      );

      if (orders.results !== 1) {
        api.throw(404, { message: 'Order not found', operationId });
      }

      const order = orders.data[0];

      if (order.status !== 'PENDING') {
        api.throw(409, {
          message: 'Order is not eligible for reservation',
          operationId,
          currentStatus: order.status,
        });
      }

      const balances = await tx.getDatabaseData<Balance>(
        'balances',
        {
          filter: {
            and: [
              { field: 'accountId', op: 'eq', value: order.accountId },
              { field: 'currency', op: 'eq', value: order.currency },
            ],
          },
          take: 1,
        },
        { lock: 'update' },
      );

      if (balances.results !== 1) {
        api.throw(409, {
          message: 'Balance is unavailable',
          operationId,
        });
      }

      const balance = balances.data[0];
      const amountMinor = BigInt(order.amountMinor);
      const availableMinor = BigInt(balance.availableMinor);
      const reservedMinor = BigInt(balance.reservedMinor);

      if (amountMinor <= 0n || availableMinor < amountMinor) {
        api.throw(409, {
          message: 'Insufficient available balance',
          operationId,
        });
      }

      const transactionId = util.randomUUID();

      await tx.updateDatabaseData(
        'balances',
        { balanceId: balance.balanceId },
        {
          availableMinor: (availableMinor - amountMinor).toString(),
          reservedMinor: (reservedMinor + amountMinor).toString(),
          lastOperationId: operationId,
        },
      );

      await tx.updateDatabaseData(
        'orders',
        { orderId: order.orderId, status: 'PENDING' },
        {
          status: 'RESERVED',
          reservationTransactionId: transactionId,
          lastOperationId: operationId,
        },
      );

      await tx.insertDatabaseData('transactions', [
        {
          transactionId,
          requestKey,
          orderId: order.orderId,
          accountId: order.accountId,
          type: 'RESERVATION',
          amountMinor: amountMinor.toString(),
          currency: order.currency,
          status: 'PENDING_SETTLEMENT',
          operationId,
          requestedBy: principal.userId,
        },
      ]);

      return {
        duplicate: false,
        transactionId,
        originalOperationId: undefined,
        accountId: order.accountId,
        amountMinor: amountMinor.toString(),
        currency: order.currency,
      };
    },
    {
      isolationLevel: 'SERIALIZABLE',
      timeoutMs: 15_000,
    },
  );
} catch (error) {
  await api.releaseIdempotencyKey(gateKey);
  throw error;
}

const jobId = reservation.duplicate
  ? undefined
  : await api.triggerJob(settleOrderTemplateId, {
      orderId,
      transactionId: reservation.transactionId,
      parentOperationId: operationId,
    });

api.log(
  {
    message: 'Order balance reserved',
    args: {
      operationId,
      orderId,
      transactionId: reservation.transactionId,
      jobId,
      requestedBy: principal.userId,
    },
  },
  'INFO',
);

return {
  operationId,
  transactionId: reservation.transactionId,
  jobId,
  duplicate: reservation.duplicate,
  originalOperationId: reservation.originalOperationId,
  status: reservation.duplicate ? 'ALREADY_RESERVED' : 'RESERVED',
};
```

> [!WARNING]
>
> `api.transactionDatabase()` commits when its callback resolves and rolls back when it throws. Await each `tx.*` call sequentially; parallel transaction operations are rejected. The default timeout is 15 seconds, and explicit `timeoutMs` values are limited to 1–30 seconds.

The idempotency guard suppresses concurrent duplicates. The unique `transactions.requestKey` and existing ledger row are the durable business truth after the cache entry expires.

## Configure the Endpoint

Open **Endpoints**, create the definition inactive, and configure it before activation.

| UI field | Value for this guide | Reason |
| - | - | - |
| Name | `Reserve order balance` | Searchable operator name. |
| Method | `POST` | The command changes state. |
| Path | `/orders/reserve` | Stable public route. |
| Component | `Reserve order balance` | Selects the reviewed logic. |
| Component Version | Exact reviewed version | Prevents an unrelated activation changing the contract. |
| Runtime input | `settleOrderTemplateId` | Keeps deployment configuration outside caller-controlled input. |
| Request guard | `reserveOrderRequest` | Rejects malformed input before execution. |
| Validation status | `422` | Separates invalid input from business conflicts. |
| Success status | `200` | The balance reservation is complete when returned. |
| Hide validation details | On for public clients | Avoids exposing internal schema details. |
| Hide request | On | Reduces retained sensitive payload data. |
| Hide response | Off or policy-driven | Keeps the returned IDs available to authorized support. |

Use the smallest practical timeout and memory budget. Inspect the generated OpenAPI operation and exercise both accepted and rejected requests while the definition remains inactive.

### Request

```bash theme={null}
curl --request POST \
  --url "$REVO_ENDPOINT_URL/orders/reserve" \
  --header "Authorization: Bearer $REVO_SERVICE_ACCOUNT_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "orderId": "0195af19-9b41-7a7c-8c5c-35ab875e165c",
    "requestKey": "checkout-0195af19-9b41-7a7c-8c5c-35ab875e165c-v1"
  }'
```

### Response

```json theme={null}
{
  "operationId": "0195af23-78d0-7c6c-b444-82238598e122",
  "transactionId": "0195af23-7a2e-7567-a9a2-8ab735cf3788",
  "jobId": "0195af23-7b7a-7d17-ae86-c4b16d8e47ec",
  "status": "RESERVED"
}
```

Also retain the `x-revo-oid` response header in the calling system. It is the fastest starting point for a cross-service trace when a client did not preserve the JSON response.

Continue with [first production workflow](https://docs.revoengine.com/platform/first-production-workflow.md).

Previous: [step 1](https://docs.revoengine.com/tutorials/order-state.md). Next: [step 3](https://docs.revoengine.com/tutorials/order-settlement.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.