# Architecture Decision Records for Agents

> Source: <https://dev.to/diyaburman/architecture-decision-records-for-agents-1jo>
> Published: 2026-08-10 13:30:00+00:00

*Building the AI Dark Factory — Issue #16*

I want to be upfront about something before we get into it. None of the frameworks in this article is mine. The ideas here come from two people who have been thinking about this stuff way harder and longer than I have — and they deserve full credit before I say another word.

Dan Shapiro — CEO of Glowforge, Wharton Research Fellow, and the person who gave this whole conversation a vocabulary. His blog post “The Five Levels: from Spicy Autocomplete to the Dark Factory” is the conceptual spine of everything I’m about to say. Read the original. It’s short, sharp, and will make you uncomfortable in the best way. [danshapiro.com](https://www.danshapiro.com/blog/2026/01/the-five-levels-from-spicy-autocomplete-to-the-software-factory)

Nate B. Jones — AI strategist, zero-hype practitioner, and the person whose YouTube channel made me realize I had been fooling myself about where I actually sat on this ladder. His video “The 5 Levels of AI Coding (Why Most of You Won’t Make It Past Level 2)” is what triggered this entire newsletter. [natebjones.com](https://www.natebjones.com/) — [Watch the video](https://youtu.be/bDcgHzCBgmQ)

This newsletter — The Level 5 Engineer — is my public learning log. I’m a Senior Software Engineer and a Tech Lead, currently somewhere between Level 2 and Level 3 (in context of the title of this newsletter) on a good day. The goal is Level 5. I’m documenting the climb in real time — the frameworks, the tools, the mindset shifts, and the moments where I realize I’ve been doing it wrong. If you’re on a similar journey, pull up a chair.

Issue #15 built a production-grade CLAUDE.md with a decision index. The index points at ADR-001 and ADR-002. This issue builds them.

More importantly: this issue demonstrates the exact failure mode that makes ADRs necessary — not in theory, but in a real implementation on this project's codebase. An agent asked to optimise the order creation endpoint made a locally reasonable change that violated a load-bearing invariant. Whether the test suite caught it is the most important finding in this session.

A human-facing ADR contains: title, status, context, decision, consequences. Written for a reader who can infer implications, fill in gaps, and ask follow-up questions.

An agent cannot infer. An agent cannot ask follow-up questions in the middle of an implementation. By the time an agent has written code that violates an invariant, the violation is already embedded in a diff that looks correct. The test suite either catches it or it reaches production.

An agent-readable ADR adds four sections that a human-facing ADR does not need:

**Invariant statement.** Not what was decided — what must remain true regardless of how future changes are framed. "Inventory must be checked before payment is initiated" is not a description of the current implementation. It is a constraint on all future implementations, including ones that look like improvements.

**Dangerous improvements.** A list of changes that are locally reasonable, improve a real metric, and violate the invariant. These are the changes an agent will propose — because they are the changes a competent engineer would propose. Naming them explicitly is what distinguishes "this is a documented constraint" from "this is just how it currently happens to work."

**Agent check questions.** Yes/no questions the agent must answer before modifying any code path covered by the ADR. Not "have you considered the implications" — "does your change ensure X? yes or no?"

**Consequence table.** Specific observable outputs that signal an ADR is being violated. When Scenario 3 requires modification to pass, that is a signal. When the payment gateway stub receives a call before the inventory mock is queried, that is a signal. The table tells the agent what to watch for — not after the violation has been committed, but while it is being developed.

The decision was made in Issue #2 and first encoded in order_creation.feature Scenario 3: the inventory check must complete before any payment gateway call is initiated.

**Context:** If payment is attempted before inventory is confirmed, a customer can be charged for an order that cannot be fulfilled. The payment reversal process is more expensive, slower, and more error-prone than a pre-payment stock check. The architectural choice was: accept slightly higher latency on the order creation path in exchange for never charging a customer for an unavailable item.

**Decision:** The inventory service is called first. If inventory confirms availability, the payment gateway is called. If inventory reports unavailability, the payment gateway is never called. This ordering is non-negotiable regardless of performance characteristics.

**Invariant:** Inventory availability must be confirmed before any payment gateway call is initiated for the same order. This must hold regardless of implementation approach — sequential, concurrent, or async.

**Dangerous improvements:**

`asyncio.gather()`

or threading — looks like a latency improvement, starts the payment call before inventory result is available**Agent check:**

**Consequence table:**

The decision was made in Issue #7: the notification service call must remain asynchronous.

**Context:** Coupling order confirmation to notification delivery means a flaky or unavailable notification service blocks all order confirmations. The notification service is an ancillary concern — customers care about their order being confirmed, not about receiving a notification in the same HTTP response cycle.

**Decision:** The notification call is fire-and-forget via daemon thread. The order service does not verify delivery success. Delivery reliability is the notification service's responsibility, not the order service's.

**Invariant:** The notification service call must not block the order confirmation response. Order confirmation success must not depend on notification delivery success.

**Dangerous improvements:**

**Agent check:**

**Consequence table:**

This is the centrepiece of the issue. Not a hypothetical — a real implementation, on this codebase, run through the test suite.

**The task:** The inventory check and payment call in `create_order()`

are currently sequential. Refactor to run them concurrently using Python threading to reduce p99 latency.

**The implementation (agent without ADR):**

``` python
def create_order(req: CreateOrderRequest):
    # ... validation ...

    results = {}
    errors = {}

    def check_inventory():
        try:
            inv = httpx.post(f"{INVENTORY_URL}/inventory/check/{req.inventory_scenario}",
                            json={"skus": skus}, timeout=5.0)
            results["inventory"] = inv.json()
        except Exception as e:
            errors["inventory"] = str(e)

    def attempt_payment():
        try:
            pay = httpx.post(f"{PAYMENT_URL}/payments/charge/{req.payment_scenario}",
                            json={"amount": total, "user_id": req.user_id},
                            timeout=PAYMENT_TIMEOUT)
            results["payment"] = pay.json()
        except httpx.TimeoutException:
            errors["payment"] = "timeout"
        except Exception as e:
            errors["payment"] = str(e)

    # Run both concurrently
    inv_thread = threading.Thread(target=check_inventory)
    pay_thread = threading.Thread(target=attempt_payment)
    inv_thread.start()
    pay_thread.start()
    inv_thread.join()
    pay_thread.join()

    # Process results...
```

Reasonable. The latency argument is real — sequential calls add wait time for every order, and the common case is "in stock, payment succeeds." Running them in parallel looks like a genuine improvement.

**The test results:**

```
test_order_is_successfully_created... PASSED
test_order_is_rejected_when_payment_is_declined PASSED
test_order_is_rejected_when_an_item_is_out_of_stock FAILED
test_order_surfaces_partial_unavailability... FAILED
test_order_handling_is_graceful_when_the_payment_gateway_times_out PASSED

3 passed, 2 failed
```

Scenario 3 failed. The test caught the violation.

**Why Scenario 3 caught it:**

```
AssertionError: Expected no payment calls, got:
[{'method': 'POST', 'path': '/payments/charge/out-of-stock', 'body': '...'}]
```

The payment gateway received a charge request for an out-of-stock order. The concurrent implementation started both the inventory check and the payment call simultaneously. The inventory check returned "out of stock" and the payment call was cancelled — but not before the mock server recorded that it had been contacted. The assertion "payment gateway is never called" failed because it was called, just not completed.

**What the ADR would have prevented:**

Working through ADR-001's Agent check questions before implementation:

Q1: Does my change ensure inventory confirmation completes before any payment gateway call is initiated? **No.** Both calls start simultaneously. The payment call initiates before the inventory result is available.

The answer to Q1 is "no." The ADR check halts at Q1. The implementation is not written.

**The critical dependency this experiment revealed:**

Scenario 3 caught the violation because this project has a tight spec. The assertion "payment gateway is never called" is precise — it checks the mock server's call log, not the response body. A project with a looser spec — one that only asserted on the response body, checking that the order status was UNAVAILABLE — would have passed all five scenarios with the concurrent implementation. The payment call starts, the inventory check returns out-of-stock, the order returns UNAVAILABLE. Response body: correct. Payment gateway contacted: yes, which violates the invariant, but the test never looks at the call log.

This is the specific production failure mode the dangerous improvement creates on a project with a looser spec: the customer receives UNAVAILABLE. The payment gateway also receives a charge request that was never completed — but because the API call started and was then abandoned, the gateway may record a pending authorization. Depending on the payment provider, that authorization may hold funds for 24–72 hours. The customer's card shows a pending charge. Their order is not confirmed. Support ticket arrives.

**The revert and the correct implementation:**

After reverting the dangerous improvement, a constraint-satisfying optimisation was implemented: inventory check runs first (unchanged), payment call starts only after inventory confirms availability, but the payment retry logic was tightened to use non-blocking timeouts. The ordering invariant is preserved. The latency improvement is smaller but real.

On this project: the test suite caught the violation. Scenario 3's call-log assertion is precise enough to detect that the payment gateway was contacted before inventory confirmed availability.

On a project with a looser spec: the test suite would not catch it. The violation is in the ordering of calls — which is only detectable if you are asserting on call sequence, not just on response values.

The ADR check caught it at Q1, before any implementation was written, regardless of how tight or loose the spec is. That is the difference between the ADR and the test suite as safety mechanisms:

The test suite catches violations after implementation, and only for the behaviors it was written to test. The ADR check catches violations before implementation, for all implementations regardless of what the tests cover.

A project whose only protection against invariant violations is its test suite is protected only as well as the tests that happen to cover the invariant. A project with ADR agent check questions is protected whether or not anyone thought to write the test.

The CLAUDE.md decision index entries for inventory-before-payment and fire-and-forget notification now point at real documents. The agent check section added to CLAUDE.md states explicitly: before modifying a code path covered by an ADR, answer all Agent check questions before writing code. If any question cannot be answered yes, stop and flag rather than proceed.

The decision index is no longer a list of intentions. It is a routing table to machine-readable constraints.

The dangerous improvement experiment revealed it: both ADRs exist because someone anticipated the need for them. ADR-001 exists because Issue #2's inventory-before-payment decision was explicit and documented in a finding. ADR-002 exists because Issue #7's fire-and-forget decision was deliberate and explained.

What about the decisions that were not deliberate? The 0.3-second sleep in the notification thread — is that a documented decision or an implementation detail? The in-memory order store — is that a deliberate architectural choice or a placeholder that future sessions may replace? The mock-server-per-service architecture — is that a constraint or a convenience?

An ADR captures a decision that was made explicitly. It does not capture the decisions that were made implicitly — the choices that seemed obvious at the time, the patterns that emerged without discussion, the behaviors that became load-bearing without anyone noticing.

Issue #17 addresses this: evals as pre-flight checks that catch invariant violations before implementation, regardless of whether an ADR exists. The ADR is the artifact for documented decisions. The eval is the safety net for undocumented ones.

*Next issue: Evals as Guardrails — not QA tests, not skill reviews, but pre-flight checks that intercept agent intent before execution and ask whether this situation is safe to proceed.*

**Sources & Further Reading**

*This article was written with the assistance of AI tools.*
