8 min read

Prompt Patterns That Work on Real Code (And Why)

Six prompt patterns that hold up on a large production codebase, the reasoning behind each one, the popular advice that measurably does nothing, and the repair loop to use when an answer comes back wrong.

AIDeveloper ExperienceWorkflowAndroidKotlin
HR

Hessam Rastegari

Senior Android Developer · 12 years shipping Android

TL;DR — Six patterns carry almost all the value on real code: paste ground truth instead of describing it, constrain the shape of the answer before the content, plan before code, force alternatives with a named loser, make it state its assumptions, and teach conventions by example, not adjective. What doesn't work: role-play preambles, politeness, "think step by step" on a modern reasoning model, and dumping your whole repo in — that one actively hurts. Every pattern here does the same thing: it moves the model from deciding to producing.

Why generic prompting advice fails on a codebase

Most prompting guidance is written for open-ended generation, where the answer space is huge and the evaluation is subjective. Code is the opposite: there is usually a right answer, it's constrained by thousands of lines you didn't paste, and verification is expensive.

That changes what a good prompt is for. It isn't coaxing better prose — it's narrowing the answer space to the part where your codebase lives, and making the output cheap to check. Every pattern below is one of those two things.

1. Paste ground truth, not a description of it

The most common failure isn't the model's — it's that people summarise the problem.

"I've got a repository that fetches a user and sometimes it returns stale data."

Compare:

class UserRepository @Inject constructor(
    private val api: UserApi,
    private val dao: UserDao,
    @Named("io") private val dispatcher: CoroutineDispatcher,
) {
    fun observe(id: String): Flow<User> = dao.observe(id)
        .onStart { refresh(id) }
        .flowOn(dispatcher)

    private suspend fun refresh(id: String) {
        runCatching { dao.upsert(api.get(id)) }
    }
}
Test failure:
expected: User(name=Ada, v=2)
but was:  User(name=Ada, v=1)
at UserRepositoryTest.kt:88 (passes in isolation, fails in suite)

Your summary was lossy in exactly the region where the bug lives — here, runCatching swallowing the failure and the suite-only reproduction pointing at shared state. Whatever you paraphrase, you have already decided is irrelevant, and that's the decision you're least qualified to make while debugging.

Same rule for errors: paste the whole stack trace, including the "Caused by" chain. The frame you'd have trimmed is usually the one that matters.

2. Constrain the shape before the content

Say what the answer should look like before you say what it should contain. Unprompted, a model produces the most complete, most general version of what you asked, because that's what "good" looks like in the training data.

Reply with the modified function only. No prose,
no explanation, no imports section.
Kotlin, no new dependencies, keep it under 40 lines.
If you'd need a new abstraction, say so instead of
adding one.

That last line is worth stealing. It converts a silent architectural decision into a sentence you can disagree with.

For anything larger, ask for a shape you can verify quickly: a diff, a table, a test. Prose is the hardest output to check and the easiest to produce, which is a bad combination.

3. Plan first, code second

For anything above about thirty lines:

Don't write code yet.
Give me the approach in five bullets, including
which files you'd touch and what you'd delete.

Rejecting a wrong plan takes ten seconds. Rejecting a wrong 400-line diff takes twenty minutes, and — this is the real cost — by then you've read enough of it to feel invested in salvaging it. The sunk-cost pull on generated code is genuinely strong and worth designing around.

The plan is also where you catch the misunderstanding cheaply. If bullet three says "add a caching layer to UserRepository" and you already have one two modules up, you found that in a sentence instead of in a diff.

4. Force alternatives, and make it name a loser

The highest-value pattern here, and the one fewest people use:

Three ways to do this. For each: one line on how it
works, and one line on what it's WORSE at than the
others. Then pick one and say why.

Two things happen. First, it defeats the first-plausible-answer failure — the model's initial response is strongly shaped by what's most common in training data, which is not the same as what's best for a codebase with your constraints. Second, and more usefully: the trade-off list is frequently more valuable than the code. You're the one who knows which downside you can absorb; you just needed the options enumerated.

Insisting on the worse at line matters. Ask for "pros and cons" and you get three options that all sound fine. Ask what each one loses and the answers separate.

5. Make it state its assumptions

Before the answer, list every assumption you're
making about my codebase. Then answer.

This is the closest thing to a reliability knob that exists in this workflow.

Hallucinations about your code — a method that doesn't exist, a library version you don't use, a threading guarantee you never made — arrive as a checkable bullet list instead of arriving as a bug in week three. Scanning five assumptions takes five seconds. Two of them will be wrong, and the correction is a one-line reply.

It also surfaces the interesting wrong assumptions. "I assumed AppDatabase is a singleton" is worth reading, because when it's false, everything downstream was going to be false.

6. Teach conventions by example, never by adjective

"Idiomatic", "clean", "production-ready", "following best practices" — these are all approximately zero information, because they resolve to an average of the internet rather than to your team.

Instead:

Here are two files that do this correctly in our
codebase: [paste]. Match their error handling,
naming and test style. Deviate only where you have
a specific reason, and say what it is.

Adjectives get you the average. Examples get you your codebase. This is also why a well-maintained conventions file in the repo pays for itself — you paste it once per session and every subsequent answer lands closer.

What doesn't work

Worth saying plainly, because these are everywhere:

  • Role-play preambles. "You are a senior Android engineer with 15 years of experience" does essentially nothing on a code task. The content of your question determines the answer; the costume doesn't.
  • Politeness, threats, or urgency. No effect. Be brief instead — it saves your time.
  • "Think step by step." Largely obsolete on modern reasoning models, which already do. Harmless, just not the lever it was.
  • Dumping the whole repo in. This one actively hurts. More irrelevant context means more opportunity to anchor on the wrong file. Curate to the five files that matter — the interface, two call sites, the test, the error. Selecting those five is the actual skill, and doing it usually teaches you something about the bug.
  • Asking it to decide. "Should we use Hilt or Koin?" gets you a balanced summary of a debate. "Here are our three constraints, which one violates fewest?" gets you something usable. The pattern that fails is asking for a verdict; the pattern that works is asking for material for your verdict.

The repair loop

When the answer is wrong — which is normal, not a failure of the process — most people reply "that's wrong, try again". That's the worst available move: it removes information and invites a random re-roll.

Instead, add what you learned:

That fails with: [new error].
So the DAO is not returning a fresh Flow after
upsert — the emission never arrives.
Given that, what's the next thing to check?

You've turned a rejection into new evidence, which is what actually moves the thread forward.

And know when to abandon it. If two repairs haven't worked, start a new conversation. A thread that has gone wrong twice is anchored on its own wrong answers, and every further message argues against a transcript rather than the problem. Restating cleanly, with what you now know, beats the tenth message of a bad thread almost every time — cheaper, too.

The honest limit

No prompt supplies context that doesn't exist. It doesn't know your backend's undocumented behaviour, the outage that caused this defensive code, or that the feature is being deleted next quarter. It will produce a confident, competent answer to the question you asked and never tell you it was the wrong question.

That check is yours, and no pattern in this article replaces it.

The rule

Every pattern here moves the model from deciding to producing. Ground truth over description, shape before content, plan before code, alternatives with a named loser, assumptions on the table, conventions by example. Prompting isn't a magic phrasebook — it's the discipline of refusing to outsource the judgement while happily outsourcing the typing.

Keep reading