---
title: "Caine Nielsen • Dispositional Logging: Recording Not Just What Happened, But What Was Decided | Caine&#x27;s home base"
canonical_url: "https://cainenielsen.com/blog/dispositional-logging:-recording-not-just-what-happened,-but-what-was-decided"
last_updated: "2026-08-28T18:15:46.685Z"
meta:
  author: "Caine Nielsen"
  description: "Dispositional Logging: Recording Not Just What Happened, But What Was Decided"
  "og:description": "Dispositional Logging: Recording Not Just What Happened, But What Was Decided"
  "og:title": "Caine Nielsen • Dispositional Logging: Recording Not Just What Happened, But What Was Decided"
  "twitter:description": "Dispositional Logging: Recording Not Just What Happened, But What Was Decided"
  "twitter:title": "Caine Nielsen • Dispositional Logging: Recording Not Just What Happened, But What Was Decided"
---

[Back to the blog](https://cainenielsen.com/blog)

Software

# Dispositional Logging: Recording Not Just What Happened, But What Was Decided

Caine Nielsen · August 19, 2026

Most logging in a typical service answers one question: what happened? A request came in. A query ran. An error was thrown. A response went out. That's useful, but it leaves a gap around a different and often more important question: why did the system decide to do what it did?

Dispositional logging closes that gap. It's the practice of explicitly logging the disposition — the decision, outcome, or resolution — reached at a meaningful branch point in your code, along with the inputs and reasoning that led to it. Instead of inferring intent after the fact from a pile of trace lines, you write down the verdict at the moment it's reached.

## What it is

Think about the difference between these two log lines for the same event:

text

```
INFO  processing payment for order 48213
```

text

```
INFO  disposition=DECLINED order=48213 reason=insufficient_funds
      rule=balance_check available_cents=1200 required_cents=4599
```

The first tells you a payment was processed. The second tells you what the system concluded, which rule produced that conclusion, and the exact values that drove it. That second line is a dispositional log.

The pattern shows up naturally at any point in a system where code makes a choice with consequences:

- A fraud check approves, flags, or rejects a transaction
- A routing layer picks a region, a partition, or a fallback provider
- A job scheduler decides to retry, dead-letter, or drop a message
- A feature-flag evaluation resolves to on/off for a given user
- A pest control job (to pick an industry close to home) gets marked completed, rescheduled, or unable-to-service, and the system records why

In every case, there's a decision object: an outcome plus the reasoning behind it. Dispositional logging just means treating that decision as a first-class thing worth writing down, not something you leave the reader to reconstruct from context.

## Why it's helpful

It answers "why" questions directly, instead of by inference. When someone asks "why was this order declined" or "why did this job get retried a fourth time," a dispositional log gives you the answer in one line. Without it, you're stitching together a request log, a database log, and tribal knowledge of the business rules to guess at what the code was thinking.

It separates decisions from noise. A service can emit hundreds of ordinary trace lines per request. If the outcome and its reasoning are logged with a consistent, greppable shape (a disposition field, a stable set of reason codes), you can filter straight to the decisions and skip the noise — which matters a lot when you're debugging in production at 2am.

It creates an audit trail for free. Anywhere a decision has business, compliance, or financial consequences — approvals, rejections, access grants, refunds — a dispositional log is effectively a lightweight audit record. You get "who/what decided this and on what basis" without building a separate audit subsystem.

It surfaces bad rules, not just bad requests. Once dispositions are structured and queryable, you can aggregate them: what fraction of requests hit reason=rate\_limited this week? Is DECLINED trending up for a particular rule? That's a different, more useful signal than counting 500s.

It makes non-error outcomes debuggable. Traditional logging is skewed toward errors — you log when something throws. But plenty of "this went wrong" situations for a user aren't errors at all from the system's point of view: a legitimate decline, a valid retry, a correct-but-surprising routing choice. Dispositional logging captures those cleanly instead of forcing them through error-shaped logging or, worse, not logging them at all.

## How to add it to your code

#### 1. Identify the decision points, not every branch

Not every if statement deserves a dispositional log — that's just noisy tracing again. Look for points where the code reaches a conclusion that a human might later ask about: approved/declined, retry/drop, route A/route B, eligible/ineligible. A good heuristic: if a teammate might file a support ticket asking "why did X happen," that's a decision point worth logging.

#### 2. Give every disposition a stable shape

Define a small, consistent structure so dispositions are queryable across the codebase, not just readable in isolation. At minimum:

- disposition — the outcome itself, from a fixed enum (APPROVED, DECLINED, RETRY, DEAD\_LETTERED, …)
- reason — a stable code explaining why, not a free-text sentence that changes every time someone edits the message
- the key inputs that drove the decision
- a correlation/request ID so the disposition can be tied back to the rest of the trace

Go example, using structured logging (slog):

go

```
type Disposition string

const (
	DispositionApproved Disposition = "APPROVED"
	DispositionDeclined Disposition = "DECLINED"
)

type Reason string

const (
	ReasonInsufficientFunds Reason = "insufficient_funds"
	ReasonRiskScoreTooHigh  Reason = "risk_score_too_high"
)

func evaluatePayment(ctx context.Context, order Order, balance int64) Disposition {
	if balance < order.AmountCents {
		slog.InfoContext(ctx, "payment disposition",
			"disposition", DispositionDeclined,
			"reason", ReasonInsufficientFunds,
			"order_id", order.ID,
			"available_cents", balance,
			"required_cents", order.AmountCents,
		)
		return DispositionDeclined
	}

	slog.InfoContext(ctx, "payment disposition",
		"disposition", DispositionApproved,
		"order_id", order.ID,
	)
	return DispositionApproved
}
```

TypeScript example, using a structured logger like pino:

ts

```
type Disposition = "APPROVED" | "DECLINED";
type Reason = "insufficient_funds" | "risk_score_too_high";

function evaluatePayment(order: Order, balance: number, logger: Logger): Disposition {
  if (balance < order.amountCents) {
    logger.info({
      disposition: "DECLINED" satisfies Disposition,
      reason: "insufficient_funds" satisfies Reason,
      orderId: order.id,
      availableCents: balance,
      requiredCents: order.amountCents,
    }, "payment disposition");
    return "DECLINED";
  }

  logger.info({
    disposition: "APPROVED" satisfies Disposition,
    orderId: order.id,
  }, "payment disposition");
  return "APPROVED";
}
```

The key move in both examples: the disposition and reason are typed/enumerated, not ad-hoc strings, so they stay stable enough to filter and aggregate on later.

#### 3. Log the disposition once, at the point of decision

Resist the temptation to log the same disposition again downstream "just in case." Log it once, right where the decision is made, and pass the correlation ID along. If you need to know the disposition later in the request lifecycle, look it up by that ID rather than re-logging it — otherwise you end up with drift between the "real" decision and stale copies of it logged elsewhere.

#### 4. Make dispositions queryable, not just readable

If you're on structured logging shipped to something like CloudWatch Logs Insights, Postgres (for domain events), or an ELK/OpenSearch stack, this pays off fast:

text

```
-- Postgres: what's driving declines this week?
SELECT reason, count(*)
FROM payment_dispositions
WHERE disposition = 'DECLINED'
  AND created_at > now() - interval '7 days'
GROUP BY reason
ORDER BY count(*) DESC;
```

text

```
# CloudWatch Logs Insights
fields disposition, reason, order_id
| filter disposition = "DECLINED"
| stats count(*) by reason
```

If dispositions matter enough to be audited or reported on, it's often worth writing them to a table (a job\_dispositions or payment\_dispositions table) in addition to the log line — logs are great for debugging, but a queryable table survives log retention windows and is much friendlier for dashboards.

#### 5. Keep reason codes stable and centrally defined

Reason codes are only useful if they don't drift. Define them once (an enum, a shared package, a lookup table) and treat adding a new one as a deliberate decision, the same way you'd treat adding a new error type. A reason field with fifty near-duplicate free-text variants of "not enough money" is worse than no reason field at all.

## The takeaway

Ordinary logging tells you the system's actions. Dispositional logging tells you the system's conclusions, and the reasoning behind them. It's a small discipline — pick your decision points, give the outcome and reason a stable shape, log it once — but it pays for itself the first time someone asks "why did this happen" and you can answer with a query instead of an archaeology dig through trace logs.

As always, thank you for reading. I really appreciate it. 💖