Most developers use LLMs backwards. We describe what we want in five words, get 200 lines of code back, and spend the next hour debugging assumptions the model made.

I spent three months running the opposite workflow: writing a full design explanation before asking for a single line of code. The resulting code had fewer bugs, but the real improvement was in my own understanding.

What Is Literate Programming With LLMs?

Donald Knuth coined “literate programming” in 1984. The idea was simple: write prose that explains your program, then weave code into that prose. Most developers ignored it because maintaining two artifacts was tedious. LLMs change that calculus. The prose is not documentation anymore. It is the prompt.

When you explain your approach to an LLM before asking for code, you are doing something different from standard prompt engineering. You are not optimizing tokens for the model. You are forcing yourself to articulate constraints, edge cases, and dependencies before the model hallucinates an architecture for you. The LLM becomes a rubber duck that talks back, but only after you have done the actual thinking.

The Experiment: 30 Days of Explaining First

I picked 12 real tasks from my backlog, ranging from a CSV parser to a WebSocket reconnection strategy. For each one, I used two approaches:

  1. The shortcut: Paste the ticket description into the LLM and ask for code.
  2. The explanation: Write 300 to 500 words describing the problem, the constraints, the error handling strategy, and the test cases. Then ask the LLM to implement that design.

I evaluated the output on three criteria: bugs found in code review, time to merge, and number of follow-up prompts needed to fix issues.

The numbers were lopsided. Explanation-first code required 1.2 follow-up prompts on average, compared to 4.7 for the shortcut approach. Code review caught 0.3 bugs per explanation-first submission versus 2.1 bugs for shortcut code.

The catch was time. Writing the explanation added 15 to 20 minutes to the front of every task. Whether that pays off depends on what you are building.

How to Structure an Explanation That Actually Works

A good explanation is not just a longer prompt. It is a design document with a specific structure. Here is the template I refined after the first week:

## Problem
[What are we solving and why?]

## Constraints
[What can't we do? What formats, protocols, or dependencies must we respect?]

## Edge Cases
[What happens when input is null, empty, malformed, or hostile?]

## Error Handling Strategy
[Fail fast or fail graceful? What do we log? What do we return?]

## Test Cases
[The specific inputs and expected outputs that prove this works.]

Here is a real example. I needed a function that validates a webhook signature using HMAC-SHA256, but with a twist: the signature header could use two different formats depending on which service sent the request.

Problem: Validate incoming webhook signatures. Some senders use X-Signature: sha256=<hex> and others use X-Signature: <hex> without the prefix. We need to support both without breaking existing integrations.

Constraints: Must use the Node.js crypto module. Must use constant-time comparison to prevent timing attacks. Must return a clear boolean, never throw on malformed input.

Edge Cases: Header is missing. Header is empty string. Header has unknown prefix like sha512=. Hex string is odd-length or contains non-hex characters.

Error Handling Strategy: Log the specific failure reason at debug level, but return false for all invalid cases. Never leak signature details in error messages.

Test Cases:

  1. Valid sha256=abc123... matches computed signature.
  2. Valid bare abc123... matches computed signature.
  3. Missing header returns false.
  4. Wrong signature returns false.
  5. Non-hex characters in signature return false.

Then I appended: “Implement this function in TypeScript.”

The model returned this:

import { createHmac, timingSafeEqual } from 'crypto';

export function validateWebhookSignature(
  payload: string,
  header: string | undefined,
  secret: string
): boolean {
  if (!header || header.length === 0) {
    return false;
  }

  const hexPattern = /^[0-9a-fA-F]+$/;
  let signatureHex: string;

  const prefixMatch = header.match(/^sha256=(.+)$/);
  if (prefixMatch) {
    signatureHex = prefixMatch[1];
  } else {
    signatureHex = header;
  }

  if (!hexPattern.test(signatureHex) || signatureHex.length % 2 !== 0) {
    return false;
  }

  const expected = createHmac('sha256', secret).update(payload).digest();
  const actual = Buffer.from(signatureHex, 'hex');

  if (actual.length !== expected.length) {
    return false;
  }

  return timingSafeEqual(actual, expected);
}

This compiled on the first try. More importantly, it handled every edge case I specified because I had already done the thinking. The model did not need to guess what I meant by “validate.”

Where It Helps and Where It Wastes Time

This approach shines for anything with branching logic, external dependencies, or security implications. The webhook validator above is a perfect example. The model cannot know your threat model unless you tell it.

But for boilerplate, it is overkill. I do not write 500 words to generate a React form component or a standard database migration. For those, the shortcut approach works fine because the patterns are well-represented in the training data and the stakes are low.

The real trap is the middle ground: tasks that feel simple but are not. A CSV parser seems straightforward until you encounter quoted fields containing commas, or BOM markers, or mixed line endings. These are the tasks where skipping the explanation costs you the most time later.

A Workflow You Can Steal Today

You do not need to change your entire process. Try this for one task this week:

  1. Open your LLM chat and write the explanation first. Do not mention code yet.
  2. Read your explanation back. If you cannot articulate the edge cases, stop. You are not ready to write code yet, and you definitely are not ready to have an LLM write it for you.
  3. Append your implementation request to the same message.
  4. Review the output against your own test cases, not against your intuition.

The step most people skip is number two. If your explanation is vague, the LLM will mirror that vagueness back at you in the form of subtle bugs. The explanation is a smoke test for your own understanding.

The Real Benefit Is Not the Code

After 30 days, I noticed something unexpected. The code was better, but my own design skills improved faster. Writing explanations forces you to name your assumptions explicitly. When you do that, you often realize your approach is wrong before you write a single line of code.

The LLM is still doing the typing. But you are doing the thinking, which was always the bottleneck.

If you want to try this yourself, start with your next bug fix. Before you ask an LLM for help, write two paragraphs explaining what you think is happening and why. You might solve it before you finish writing.