How I Use AI to Design and Review Complex Changes
A practical account of using codebase research, an RFC, independent model critique, and a reviewed PR stack to design and deliver a complex change without giving up human ownership.
- AI
- Engineering
The most useful moment in a recent AI-assisted project came after the code had already been generated. I was reviewing a pull request near the bottom of a dependency-ordered stack and found a design issue. Fixing that one PR would not be enough; the same assumption had worked its way into later changes. I left a review comment, asked the AI assistant to address it, and told it to look for the same pattern throughout the stack. Then I reviewed those changes too.
That is closer to how I think about AI in software development than “write a prompt and receive a feature.” The interesting part is the loop: investigate, propose, critique, implement, review, and carry what we learn forward.
Start with the problem, not the prompt
The project followed a product pivot. We needed to revive an older modeling capability and extend it to support a broader set of scenarios. The existing feature held useful domain knowledge, but much of the reasoning behind it was no longer documented. Meanwhile, a frontend prototype had helped us explore the new experience. To make that prototype real, we needed a backend model that could persist the new concepts without simply recreating the old system.
My first AI-assisted task was therefore research. I used Codex to inspect the codebase and the legacy feature, trace the relationships among the existing concepts, and surface questions that the implementation alone could not answer. I did not want a model to treat the old code as a complete specification. It was evidence: valuable, incomplete, and occasionally shaped by constraints that no longer applied.
That distinction matters. When an assistant moves directly from reading code to writing code, it can reproduce accidental complexity with remarkable speed. I wanted a shared account of the problem before asking anyone—human or model—to build the solution.
Make the RFC a working conversation
I co-developed a proposal with Codex and put it in a Notion RFC. The document described the new concepts, the relationships among them, the migration and implementation sequence, and the questions we had not settled. I then read it as I would read a colleague’s design: I commented on assumptions, asked for alternatives, and pushed back where the model’s explanation seemed too tidy.
The assistant replied to those comments and revised the RFC, but its responses were inputs to my decision, not decisions themselves. Sometimes a comment exposed a real flaw; sometimes the proposal was reasonable and needed a clearer explanation. Having the discussion in the document made those distinctions visible.
I also wanted an independent critique. I asked Claude to review the Codex-produced RFC rigorously, then brought its objections back into the design conversation. The point was not to hold a vote between models. Different models can miss different things, and they can also confidently disagree for unhelpful reasons. I compared their arguments against the requirements and the codebase, then decided which changes improved the design.
Finally, colleagues reviewed the RFC. I used AI to help respond and revise, but I monitored those exchanges closely. A design document is not “reviewed” because a comment thread has been answered. It is reviewed when the people responsible understand the tradeoffs and can see how the proposal changed.
Turn the design into reviewable increments
Once the architecture was stable enough, I translated the RFC into a dependency-ordered stack of pull requests. Each PR covered a coherent model area and built on the one below it. That ordering made the work easier to inspect: reviewers could understand the foundation before moving to code that depended on it.
The stack was AI-assisted, but I did not regard generated code as pre-reviewed code. I reviewed from the base upward, left comments on design and implementation choices, and had the assistant address them one PR at a time. When a correction applied to several later PRs, I explicitly asked it to carry the change through the stack. I then checked the resulting edits rather than assuming the instruction had been followed everywhere.
Colleagues reviewed the PRs as well. Their comments entered the same loop: understand the concern, decide on the change, use AI where it helped with the mechanics, and verify the final result. The outcome was a usable backend foundation that allowed the team to connect an already-built frontend prototype to persisted data. The complete new product had not shipped at that point, and I would not describe it as though it had. The useful result was that the architecture and implementation were sound enough to unblock the next stage of integration.
A review comment is a design signal
A stacked set of PRs changes the meaning of a review comment. If an issue appears in the first layer, I do not want to ask only, “How do we fix this line?” I want to ask, “What assumption produced this choice, and where else did we repeat it?” That question is especially important when the same assistant helped write several related changes. It can be consistently wrong as well as consistently helpful.
I handle that by separating the correction from the search for similar cases. First, I make sure I can state the underlying rule in my own words. Then I ask the assistant to find other places where the rule applies, including later PRs in the stack. I inspect the proposed changes and the tests for each affected layer. It is easy for a mechanical sweep to produce code that looks uniform but changes behavior in one place where the context is different.
Review order helps. A foundational PR should make the next PR easier to understand, not force a reviewer to hold the entire future system in their head. When a later PR reveals that the base abstraction is awkward, I would rather revisit the base than patch around it repeatedly. A stack is useful because it exposes dependencies; it is not a promise that the earliest decision can never change.
The same discipline applies to model critique of the RFC. An objection from Claude or Codex is not automatically a requirement. I ask what failure the objection predicts, whether that failure is possible in this product, and what evidence would resolve it. Some objections produce a design change. Others become a documented tradeoff or an explicit follow-up question for a colleague. That filtering is part of the engineering work, not a delay before it.
How I know a PR is ready for another reviewer
I want the purpose of each PR to be legible without a private explanation from me. A reviewer should be able to see the concept it introduces, its relationship to earlier layers, and the decision it leaves for later work. I check that the code and tests support the description; a tidy PR summary cannot compensate for an implementation that is difficult to reason about.
I also try to keep generated changes open to challenge. If an AI tool proposes a convenient abstraction, I ask whether it reflects a durable domain concept or merely makes the current code easier to generate. If it proposes a migration path, I ask what happens to existing data and what can be rolled back. If it writes tests, I ask whether they would fail when the intended behavior breaks. These questions are ordinary software review questions, and they matter more—not less—when producing code becomes cheap.
That is the productivity benefit I value: more time and attention available for decisions, provided I actually spend it there. The PR stack helped us move through a substantial backend design efficiently, but the process worked because each layer remained discussable and revisable.
What the tools did—and what they did not
Codex was useful for codebase analysis, RFC iteration, and implementing a sequence of related changes. Claude gave me an independent architectural critique. I also use Cursor for smaller, precise in-editor work and for parallel codebase research while a larger task is underway. The choice depends on the work; I do not think one tool needs to own every step.
The surrounding environment matters too. Documentation, repository conventions, reusable agent skills, and MCP integrations can give an assistant more reliable context. That makes it easier to ask a precise question and harder to lose important constraints between tasks. It does not remove the need to verify what the assistant understood.
I still owned the architecture, the tradeoffs, the review, and the decision to accept a change. AI made it easier to explore alternatives and apply consistent edits across related work. It could not tell us whether the product model was right for the people who would use it.
Where this approach earns its keep
This workflow has a cost. An RFC, an independent critique, a PR stack, and repeated review would be unnecessary for a small stylistic change. I reach for it when the work is interdependent, the domain is not fully documented, and a mistake near the foundation would be repeated throughout the implementation.
For that kind of project, the speed gain is not just faster typing. It is the ability to make uncertainty visible early, keep a record of decisions, and apply what reviewers learn consistently across a connected body of work.
My working rule is simple: use AI to increase the amount of useful thinking and feedback a team can bring to a problem, not to skip the thinking and feedback altogether.
