Contracts — Example: Bank Transfer System

The Scenario

A banking system where customers transfer money between their own accounts and to other people's accounts. Wire transfers to external banks are supported. Daily limits and fraud detection are enforced. Every transaction must be auditable.

This is the highest-stakes contract environment. A vague contract in a bank means money appears, disappears, or doubles. There is zero tolerance for ambiguity.


Contract 1: Internal Transfer (Between Own Accounts)

CONTRACT: TransferBetweenOwnAccounts

ACCEPTS:
  - customer_id: text — required, authenticated customer
  - source_account_id: text — required, must belong to customer_id
  - destination_account_id: text — required, must belong to customer_id
  - amount: currency — required, must be positive, two decimal places maximum
    (e.g., 100.00, not 100.001)
  - memo: text — optional, max 140 characters (customer's note for their records)

RETURNS:
  - transfer_record:
    - transfer_id: text (unique, used for all future references to this transaction)
    - source_account_id: text
    - source_new_balance: currency
    - destination_account_id: text
    - destination_new_balance: currency
    - amount: currency
    - memo: text or empty
    - executed_at: timestamp (precise to millisecond)
    - status: "completed"

ERRORS:
  - customer_id not authenticated → error: "Authentication required"
  - source_account_id not found → error: "Account not found"
  - source_account does not belong to customer → error: "Account not found" 
    (IMPORTANT: same message as "not found" — never reveal that the account 
    exists but belongs to someone else)
  - destination_account_id not found → error: "Account not found"
  - destination_account does not belong to customer → error: "Account not found"
  - source and destination are the same account → error: "Source and destination 
    must be different accounts"
  - amount is zero or negative → error: "Amount must be greater than zero"
  - amount has more than 2 decimal places → error: "Amount must not exceed 
    two decimal places"
  - insufficient funds (source balance < amount) → error: "Insufficient funds. 
    Available balance: [X]"
  - source account is frozen → error: "Account is restricted. Contact support."
  - destination account is frozen → error: "Destination account is restricted. 
    Contact support."
  - daily transfer limit exceeded → error: "Daily transfer limit of [X] reached. 
    Transferred today: [Y]. Remaining: [Z]. Resets at midnight [timezone]."
  - database unreachable → error: "Service temporarily unavailable. Your transfer 
    has not been processed. Please try again."

SIDE EFFECTS:
  - Source account balance decreased by amount (atomic operation)
  - Destination account balance increased by amount (atomic operation)
  - Two transaction records created (one debit, one credit) — both reference
    the same transfer_id for traceability
  - Daily transfer running total updated for this customer
  - Transaction event logged with full details (all inputs, all outputs, 
    timestamp, IP address, device info)
  - If amount exceeds $10,000: regulatory reporting flag set (Currency 
    Transaction Report required by law)

Critical Design Point: Atomicity

The most important word in this contract is "atomic." Both the deduction from the source and the addition to the destination must happen as a single, indivisible operation. You cannot have a state where:

  • Money has left the source but not arrived at the destination (money lost)
  • Money has arrived at the destination but not left the source (money created)

This is the same "two-phase commit" concept from the ATM example. The contract specifies atomic behavior, and the implementation must guarantee it — how it does so is an implementation detail, but the guarantee is part of the contract.

Critical Design Point: Security in Error Messages

Notice that "account not found" and "account doesn't belong to you" return the same error message. This is intentional. If the system said "account 12345 exists but doesn't belong to you," an attacker could probe for valid account numbers. The contract uses identical error messages for different failure reasons to prevent information leakage.


Contract 2: Transfer to Another Person

CONTRACT: TransferToOtherCustomer

ACCEPTS:
  - customer_id: text — required, authenticated customer (the sender)
  - source_account_id: text — required, must belong to customer_id
  - recipient_identifier: one of:
    - account_number: text (direct account number)
    - email: text (if recipient has registered email for receiving transfers)
    - phone: text (if recipient has registered phone for receiving transfers)
  - amount: currency — required, positive, two decimal places max
  - memo: text — optional, max 140 characters

RETURNS:
  - transfer_record:
    - transfer_id: text
    - source_account_id: text
    - source_new_balance: currency
    - recipient_display: text (recipient's name, partially masked: "J*** Smith")
    - amount: currency
    - memo: text or empty
    - status: one of:
      - "completed" (instant transfer, recipient is at the same bank)
      - "pending" (recipient at external bank, or amount triggers review)
    - executed_at: timestamp
    - estimated_arrival: text (if pending — "within 1 business day" or 
      "within 3 business days")

ERRORS:
  (All errors from TransferBetweenOwnAccounts, PLUS:)
  - recipient not found → error: "No account found for this recipient"
  - recipient account is closed → error: "Recipient account is not active"
  - sender and recipient are the same person → error: "Use internal transfer 
    for transfers between your own accounts" (different flow, different limits)
  - amount exceeds person-to-person daily limit → error: "Person-to-person 
    daily limit of [X] reached"
  - fraud detection flag → error: "Transfer requires additional verification. 
    Please contact support or verify via [method]." 
    (The transfer is NOT processed. It is held.)

SIDE EFFECTS:
  - Source balance decreased by amount
  - If same bank and status = "completed": recipient balance increased immediately
  - If different bank and status = "pending": transfer queued for batch processing
  - If amount > $3,000 to a new recipient: additional verification step triggered
    (two-factor authentication sent to customer's phone)
  - Both sender's and recipient's transaction histories updated
  - Transfer event logged (sender's IP, device fingerprint, amount, recipient info)
  - Fraud scoring model updated with this transfer's characteristics
  - If amount > $10,000: regulatory reporting flag set
  - Notification sent to recipient (if they have notifications enabled)

Key Difference From Internal Transfer

The recipient might not get the money immediately. This introduces the concept of eventual consistency — the sender's balance changes now, but the recipient's balance might change later. The contract makes this explicit through the status and estimated_arrival fields.

Fraud detection can block the transfer. Unlike internal transfers (low risk), person-to-person transfers are a fraud vector. The contract includes a specific error for this case that instructs the caller to handle it as a "held" state — not a rejection, not a success, but a third state.


Contract 3: Wire Transfer (External Bank)

CONTRACT: WireTransfer

ACCEPTS:
  - customer_id: text — required, authenticated
  - source_account_id: text — required, must belong to customer_id
  - recipient_name: text — required, full legal name
  - recipient_bank_routing_number: text — required, 9 digits
  - recipient_account_number: text — required
  - amount: currency — required, positive, two decimal places max
  - wire_type: one of ["domestic", "international"]
    - If "international":
      - swift_code: text — required, 8 or 11 characters
      - recipient_bank_name: text — required
      - recipient_bank_address: text — required
      - recipient_country: text — required, ISO country code
  - purpose: text — required for international (regulatory requirement), 
    optional for domestic, max 200 characters
  - memo: text — optional, max 140 characters

RETURNS:
  - wire_record:
    - wire_id: text
    - source_account_id: text
    - source_new_balance: currency (amount + wire fee already deducted)
    - recipient_name: text
    - amount: currency
    - wire_fee: currency (displayed separately — $25 domestic, $45 international)
    - total_deducted: currency (amount + wire_fee)
    - status: "pending" (wires are never instant)
    - submitted_at: timestamp
    - estimated_arrival: text ("1-2 business days" domestic, 
      "3-5 business days" international)
    - confirmation_number: text (for tracking with the wire network)

ERRORS:
  (All standard account/amount errors, PLUS:)
  - routing_number invalid format → error: "Routing number must be exactly 9 digits"
  - routing_number not found in bank directory → error: "Unknown routing number. 
    Verify with recipient's bank."
  - swift_code invalid format → error: "SWIFT code must be 8 or 11 characters"
  - wire_type is "international" and purpose is empty → error: "Purpose is 
    required for international wire transfers"
  - insufficient funds for amount + wire_fee → error: "Insufficient funds. 
    Transfer amount ([X]) + wire fee ([Y]) = [Z]. Available balance: [W]."
  - wire transfer daily limit exceeded → error: "Daily wire limit exceeded"
  - customer has not completed wire transfer authorization form → error: 
    "Wire transfer authorization required. Complete enrollment first."
  - fraud or compliance hold → error: "Transfer requires manual review. 
    Expected completion: [1-2 business days]. Reference: [case_id]."

SIDE EFFECTS:
  - Source balance decreased by (amount + wire_fee) — this happens immediately
    even though the wire is "pending"
  - Wire queued for submission to Federal Reserve wire network (domestic) 
    or SWIFT network (international)
  - Compliance review automatically triggered for:
    - Any international wire
    - Domestic wires over $10,000
    - Wires to certain countries (OFAC screening)
  - Customer receives confirmation email with wire details
  - Wire event logged with full audit trail

Why Wire Contracts Are Maximally Detailed

The money leaves immediately, but the wire takes days. This creates a period where the customer's balance is reduced but the recipient hasn't received anything. The contract must make this clear — and the side effects section must document that the balance deduction is immediate even though delivery is not.

Regulatory requirements are part of the contract. Purpose is required for international wires — not because the bank wants it, but because the law requires it. The contract enforces this. If you omitted it, the implementation might skip it, and the bank could face legal penalties.

Wire fees are not optional. The contract explicitly includes the fee and shows the total deduction. A vague contract might say "returns amount" without clarifying whether the fee is included or separate — this ambiguity could cause accounting errors.


Comparing All Three Domain Examples

AspectLibraryRestaurantBank
Strictest constraintCheckout limitsFood timingAtomicity + compliance
Error message securityLow concernLow concernCritical (never reveal account existence)
Side effects count3-4 per contract4-6 per contract6-10 per contract
Regulatory requirementsMinimalHealth codes (not in contracts)Extensive (CTR, OFAC, wire auth)
Can you "undo" the operation?Yes (return the book)Partially (can void before kitchen)Depends (internal yes, wire maybe not)
Money involvedSmall finesMeal costsUnlimited
Time horizon14 days (loan period)Minutes (meal duration)Days (wire processing)
Highest-stakes error caseLost book ($25 replacement)Food allergy incidentMoney loss, legal violation

The contract structure is identical across all three: name, inputs, outputs, errors, side effects. But the rigor scales with the stakes. A missing error case in the library contract is an inconvenience. A missing error case in the bank contract is a potential financial loss or legal violation.

This is the core lesson: the contract template is universal, but the thoroughness is proportional to what's at risk.