---
id: "money-mutation-without-idempotency-key"
type: "rule"
severity: "error"
url: "https://localhost:3000/catalogue/rules/money-mutation-without-idempotency-key"
---

# Money mutation without idempotency key

An edge into a money-handling third party or a double-entry ledger declares no idempotency while the design requires exactly-once effect.

| | |
|---|---|
| Severity | error |
| Selects | every edge |
| Components in scope | 2,366 |

## Why

A call that moves money is retried after a lost response. Without an idempotency key on that hop the retry becomes a second charge, which is exactly what the design's exactly-once-effect promise rules out.

## What it checks

It looks at every edge, and fires when:

- all of these hold:
  - the edge's target node:
    - any of these hold:
      - the node's `handlesMoney` is true
      - the node's `doubleEntry` has a value
  - the edge's `idempotent` has no value
  - the design requirement `correctness.exactlyOnceEffect` is true

Message template:

```
Edge '{{edge.from}}' -> '{{edge.to}}' moves money but does not declare idempotent, while the design requires exactly-once effect. Add an idempotency key to this hop.
```

## Applies to

This rule is about an edge, so it belongs to a pair of nodes rather than one component.

2,366 catalogue components fall under it.

## Tripped by

Reference designs whose broken twin names this rule in its header:

- [coinbase-exchange](https://localhost:3000/d/zeema/coinbase-exchange)
- [hotel-reservation](https://localhost:3000/d/zeema/hotel-reservation)
- [monzo-platform](https://localhost:3000/d/zeema/monzo-platform)
- [nubank-core](https://localhost:3000/d/zeema/nubank-core)
- [payment-system](https://localhost:3000/d/zeema/payment-system)
- [stock-exchange](https://localhost:3000/d/zeema/stock-exchange)
- [stripe-payments](https://localhost:3000/d/zeema/stripe-payments)

## Source (`rule` YAML, verbatim)

```yaml
# yaml-language-server: $schema=../schema/rule.schema.json
# A call that moves money (to a third party that handles money, or into a double-entry
# ledger) will be retried after a lost response. Without an idempotency key on that hop
# the retry becomes a double charge, which the design promises never happens.
# Tripped by: designs/payment-system.broken.yaml
schemaVersion: "1"
type: "rule"
id: "money-mutation-without-idempotency-key"
severity: "error"
select:
  edge: {}
when:
  all:
    - to:
        any:
          - prop: "handlesMoney"
            equals: true
          - prop: "doubleEntry"
            exists: true
    - field: "idempotent"
      exists: false
    - requirement: "correctness.exactlyOnceEffect"
      equals: true
message: "Edge '{{edge.from}}' -> '{{edge.to}}' moves money but does not declare idempotent, while the design requires exactly-once effect. Add an idempotency key to this hop."
```

Human page: https://localhost:3000/catalogue/rules/money-mutation-without-idempotency-key
