Article 4 of 6

AI Pair Programming: Patterns That Work

How to structure your AI collaboration to produce clean, reviewable code.

14 minIntermediate
Key Takeaway

Most developers are using AI like a code vending machine — describe what you want, paste whatever comes out, move on. That produces code you can't debug and teams you can't trust. The engineers genuinely accelerating their work treat AI as a pair programmer: they maintain authorship of the design, delegate specific implementation tasks, and review every line. Five patterns — Architect First, Test-Driven AI, Scaffold and Refine, the Explainer, and the Migrator — cover the full range of how to actually do this in daily work.


Last month, I watched two engineers on my team tackle the same migration task. Both used AI assistants. One finished in three hours with clean, well-tested code that sailed through review. The other spent two days wrestling with hallucinated APIs, broken abstractions, and a pull request so tangled it took longer to review than it would have taken to write from scratch.

The difference was not the tool. It was the pattern.

Most developers are still treating AI like a code vending machine: describe what you want, paste whatever comes out, move on. This approach produces mediocre results — and worse, it produces code you don't fully own. The engineers who are genuinely accelerating their work have a fundamentally different relationship with AI. They're not delegating to it. They're pair programming with it.

"AI Writes Code for Me" vs. "AI Pair Programs with Me"

The distinction sounds subtle, but it changes everything about how you work.

"AI writes code for me" means you describe a feature, the AI generates a blob of code, and you paste it into your codebase. You're outsourcing. You've handed requirements to a contractor you've never worked with before and won't work with again. You have no idea what trade-offs they made, and you'll find out the hard way when something breaks at 3 AM.

"AI pair programs with me" means you maintain authorship of the design and intent. You use AI to accelerate specific parts of implementation while staying in the driver's seat. You're still the engineer. The AI is a collaborator who types fast, has read a lot of documentation, and never gets tired — but also has no idea what your production SLAs are.

The first approach produces code you can't confidently debug. The second produces code you shipped faster than you thought possible and understand completely.

Here are five patterns that consistently produce the second outcome.

Pattern 1: Architect First

This is the highest-leverage pattern I've found, and the one I recommend starting with every time you face a non-trivial feature.

You design the structure. Define the interfaces. Decide on module boundaries. Sketch out the data flow. Then you hand AI a very specific, constrained implementation task within that architecture.

In practice, this means writing a file with type signatures, function stubs, and comments describing what each function should do. Then you ask AI to fill in the body of each function, one at a time.

Example: You're building a notification service. You define the NotificationChannel interface, the NotificationRouter class with its method signatures, and the DeliveryStrategy enum. Then you ask AI: "Implement the route method on NotificationRouter. It should match the user's preference to the appropriate channel and call channel.send(). Handle the case where the preferred channel is unavailable by falling back to email."

The constraint is critical. AI is excellent at implementing well-defined units of work. It is poor at making architectural decisions. When you let it decide the structure, you get a tangled mess that technically works but is impossible to extend. When you give it a clearly bounded box to fill, the output is consistently good.

You retain control over the decisions that determine whether the codebase is maintainable in six months. AI handles the mechanical work that would otherwise slow you down.

Pattern 2: Test-Driven AI

Write your tests first. Then ask AI to write code that makes them pass.

This is TDD — except your pair partner can generate candidate implementations in seconds instead of minutes.

The workflow:

  1. Write a failing test that describes the behaviour you want.
  2. Ask AI to write the implementation that makes this test pass.
  3. Run the tests. If they pass, review the code. If they fail, share the error output with AI and iterate.
  4. Add the next test. Repeat.

This works remarkably well for several reasons. Your tests act as a specification — they eliminate ambiguity about what you're asking AI to produce. You have an automatic verification step. And you end up with well-tested code by default, not as an afterthought.

Example: You need a function that parses a cron expression into a human-readable schedule description. Write five tests covering standard expressions, edge cases, and invalid input. Hand these to AI and say, "Write the implementation that passes these tests." Within a minute, you have a working function with coverage on every case you care about.

The critical discipline: don't skip the review just because the tests pass. Tests verify behaviour, not code quality. AI might produce a passing implementation that's a performance problem or a maintainability nightmare six months from now. Read the code before you merge it.

Pattern 3: Scaffold and Refine

This is the most common pattern, and it works well when you're exploring unfamiliar territory or building something you haven't built before in exactly this way.

Let AI generate a rough first draft. Then refine it together through iterative conversation.

The workflow:

  1. Describe the feature or component at a high level.
  2. Let AI generate a first pass.
  3. Read the output critically. What's wrong? What's missing? What doesn't match your codebase conventions?
  4. Give specific feedback: "Extract the validation logic into a separate function." "Use our existing HttpClient wrapper instead of raw fetch." "This needs error handling for the case where the database connection drops mid-request."
  5. Iterate until the code meets your standard.

This mirrors how you'd work with a junior developer. You wouldn't expect their first draft to be production-ready. You'd expect a reasonable starting point to refine together.

Where people go wrong: They accept the first draft. Or they give vague feedback like "make it better" instead of specific, actionable direction. The quality of AI output is directly proportional to the quality of your feedback. If you wouldn't accept vague feedback from a code reviewer, don't give vague feedback to your AI pair.

Pattern 4: The Explainer

Before you modify unfamiliar code, use AI to understand it.

This isn't a code generation pattern — it's a comprehension pattern. And it might be the one that saves you the most time on a legacy codebase.

The workflow:

  1. Paste a function, class, or module you need to modify but don't fully understand.
  2. Ask AI to explain what it does, step by step.
  3. Ask follow-up questions: "Why would the author use a recursive approach here instead of iterative?" "What happens if retryCount exceeds the limit?" "Is this thread-safe under concurrent requests?"
  4. Once you understand the code, make your modifications yourself — or use one of the other patterns.

Where this shines: You've inherited a legacy data pipeline with a 400-line transformation function. Before rewriting it, paste it into your AI assistant and ask for a walkthrough. In five minutes, you understand the business logic embedded in those 400 lines. You discover three edge cases you'd have missed if you'd just started rewriting. I've used this pattern repeatedly when onboarding to new codebases — it compresses what used to take weeks of exploration into hours.

Reading code is harder than writing code. AI dramatically compresses the time to comprehend unfamiliar systems.

Pattern 5: The Migrator

Mechanical migrations are where AI pair programming delivers the most obvious, measurable ROI.

Framework upgrades. API version changes. Replacing one library with another. Updating hundreds of files to match a new coding convention. These tasks are tedious, error-prone, and mind-numbing for humans. They're perfect for AI.

The workflow:

  1. Identify the migration pattern. For example: "Replace all instances of moment().format() with dayjs().format(), accounting for the API differences in duration handling."
  2. Show AI a before-and-after example from one file.
  3. Feed it files one at a time (or in small batches) and let it apply the pattern.
  4. Review each transformation. Run tests after each batch.

I once used this pattern to migrate 80+ Angular components from a deprecated HTTP interceptor pattern to a new one. The migration took an afternoon instead of a week. Every transformation was consistent because AI doesn't get fatigued or sloppy on file number 73 the way any human inevitably does.

The discipline: Always provide a reference example. Never assume AI knows your specific migration requirements without seeing a concrete before-and-after. And always run your test suite between batches — mechanical doesn't mean risk-free.

When AI Pairing Breaks Down

Knowing when not to use these patterns is just as important as knowing the patterns themselves.

Novel algorithms. If you're inventing something new — a custom ranking algorithm, a domain-specific optimisation — AI has no reliable training data to draw from. It will confidently produce something that looks plausible but is subtly wrong in ways that only manifest at the edge cases you care about most. Do the hard thinking yourself.

Complex business logic. When logic involves deep domain knowledge, regulatory requirements, or nuanced rules that aren't documented anywhere, AI fills the gaps with assumptions. Those assumptions will be wrong in your specific context. Business logic is where your expertise matters most and AI's confidence is least trustworthy.

Security-critical code. Authentication flows. Encryption implementations. Access control logic. Permission boundaries. Don't trust AI-generated code in these areas without expert review. The cost of a subtle bug here isn't a Jira ticket — it's a breach.

Highly stateful systems. Code managing complex state transitions, distributed locks, or concurrent access patterns requires a level of reasoning about temporal behaviour that current AI models handle poorly. You'll spend more time debugging AI output than you'd have spent writing it yourself.

The general rule: the more a task requires judgment, the less useful AI pairing becomes. The more it requires mechanical execution of well-understood patterns, the more useful it becomes.

The Review Discipline

The rule I enforce on my team: review AI-generated code as if a junior developer wrote it.

You don't skip the review because "the AI is smart." You read every line. You check for:

  • Correctness. Does it actually do what you asked? AI is excellent at producing code that looks right but mishandles edge cases in subtle ways.
  • Security. SQL injection vectors? Hardcoded secrets? Overly permissive configurations? AI reproduces patterns from its training data, and a lot of that training data has security holes.
  • Performance. Unnecessary database calls? Objects being created in tight loops? O(n²) where O(n log n) would work?
  • Maintainability. Does it follow your team's conventions? Would a new team member understand it six months from now without a Slack explanation?
  • Phantom dependencies. Did AI invent a library that doesn't exist? This happens more often than you'd expect. Always verify that imports are real before you commit.

The engineers who get burned by AI are the ones who stop reviewing. The moment you trust AI output implicitly is the moment you start shipping bugs you don't understand and can't explain.

Integrating AI Pairing Into Your Daily Flow

After a year of refining this, here's the daily rhythm that works:

Morning planning (15 minutes). Review today's tasks. For each one, decide which pattern fits best. Some tasks are pure human work. Some are ideal for Architect First. Some are migrations. Making this decision upfront prevents the unstructured "let me just ask AI" impulse that leads to poor-quality output.

Coding sessions (focused blocks). Keep your AI interactions focused and contextual. Don't open a chat and dump your entire problem. Break it into discrete, well-scoped requests. One function at a time. One test at a time. One file at a time.

End-of-day review (20 minutes). Before you push, review every piece of AI-assisted code from that day. Read it fresh. This is when you catch the subtle issues — the error handling path that silently swallows exceptions, the variable name that doesn't match convention, the test that asserts the wrong thing.

Weekly retro (5 minutes, for yourself). What did AI help with this week? Where did it slow you down? What patterns worked? Adjust. This is how you get better at AI pairing over time instead of plateauing at a mediocre baseline.

Key Takeaways

  • AI pairing is collaboration, not delegation. You maintain authorship of the design and intent. AI accelerates specific implementation tasks within your architecture.
  • Match the pattern to the task. Architect First for new features, Test-Driven AI for specified behaviour, Scaffold and Refine for exploration, the Explainer for legacy comprehension, the Migrator for mechanical transformations.
  • Know when to stop. Novel algorithms, complex business logic, security-critical code, and highly stateful systems are poor fits for AI pairing. Your judgment matters most exactly where AI is least reliable.
  • Review everything as if a junior wrote it. Correctness, security, performance, maintainability, and phantom dependencies. Never skip this.
  • Your next step this week: Pick a task you're working on today that involves writing new code. Before you open a chat window, define the interface and type signatures yourself first. Then use Pattern 1 (Architect First) and compare the output quality to what you'd have gotten without the upfront structure.