“We just need to send an email after an order.” My favorite word in requests like this is “just.” While you're saying it, the work sounds almost finished: there's an order, there's the customer's address, and all that remains is connecting the two.
Then you open the project.
In one place the order is paid, in another it's packed, and in a third it's handed over for delivery. Emails are already being sent somewhere, but it's not yet clear whether you can reuse that mechanism. Before writing a few lines, you have to answer a question the original request never raised: what exactly do we mean by completing an order?
Let's use this as a worked example. We have a running service and need to add an email to the customer when an order is completed. I want to use it to show how to approach a change with a coding assistant when a project already has plenty of established behavior and you can't hold all of it in your head.
In the previous article, I suggested starting with a small change whose result you can see and check. This task is a little harder: before changing anything, we need to work out where its boundaries are.
The most tempting first request is “find the order handler and add email sending.” It sounds specific. But several decisions are already hidden inside it: that a handler is the right place, that the email should be sent from there, and that we agree on the right moment.
I'd start with what we know for certain: the customer should receive a notification when the order is completed, and email problems must not disrupt the order itself. Then I'd ask the assistant to first investigate the existing process.
We need to email the customer when an order is completed. A sending failure must not interfere with order processing. Don't change any code yet. Look at how an order moves through the system, where emails are already sent, and what checks exist nearby. Show the relevant places with references to the code. If “completed” is ambiguous, raise that as a question.
This is an example request, not a required formula. What matters to me is having a goal, a constraint, and a next step at the start of the conversation. After that request, I expect an explanation I can compare with the project.
To begin, the assistant needs access to the relevant repository and its working instructions: how the main parts are organized, how to run checks, and which conventions to follow. If I know where order processing starts, I'll provide that lead. If there's an issue describing the expected behavior, I'll share that too. Secrets and real customer data aren't needed for this: code and anonymized examples are enough.
At the same time, I don't want to explain the entire project upfront. The investigation has a route: follow the order to the relevant state and find the existing notification mechanism. Other details can wait until that route leads to them.
Suppose the assistant returns with some findings: the status changes in one module, emails go through a queue, and a similar notification already exists for another event. Now we have something we can verify. I can open those locations, read the surrounding code, and ask why this particular point was chosen.
I'd pay special attention to “completed.” The project may have several similar statuses, each meaning something different. A field name helps you search, but it doesn't define the promise you make to the customer. A “Your order is complete” email after a successful payment could be quite a surprise if the order still needs to be packed.
This is a useful point for the assistant to stop. It can show the options and explain what happens in each. Deciding which event to tell the customer about must follow the product's rules. If the answer is unknown, we need to establish it first. Otherwise, a few tidy lines will turn a guess into system behavior.
Let's say we've settled when to send the email. Now we can examine what happens around that moment. Can the handler receive the same event twice? How does the project retry failed sends? Where can we see that an email never went out?
It's easy to mistake these questions for overcomplicating a small task. But each brings us back to the same user request. The customer needs one clear notification. Running the handler again shouldn't turn into another email, and an unavailable mail service shouldn't break an order.
Here I'd ask the assistant to distinguish what it found in the code from what it's assuming. There's a huge difference between “the neighboring handler already guards against duplicates” and “systems like this usually guard against duplicates.” We can inspect and reuse the first. The second still needs checking.
Once those questions are resolved, the plan usually becomes much more concrete. We know which event matters, which sending mechanism already exists, how to add the email to it, and which scenarios to check. If something remains unknown, we can say so explicitly instead of hiding it inside the future implementation.
Before editing, I'd ask for a short account of the plan: what changes, why here, and how we'll know we got the intended behavior. That conversation helps catch unnecessary scope. If one notification suddenly calls for rebuilding the entire email module, it's worth asking whether that's really unavoidable.
Once we've agreed, we can move to code. The change needs its own branch with a clear starting state. If other tasks are running in parallel, a separate working tree—a worktree—can help. I use them to keep changes from different chats from getting mixed together. That separates the work, though the results still need to be checked for compatibility when merged.
At this point the request can be brief: implement the agreed plan, use the existing sending mechanism, and show the changes and check results. By now, most of the meaning has been assembled in the conversation and grounded in the code.
The checks should cover the behavior we agreed on. The email is produced after the right event. A repeated event is handled without creating an extra notification. A mail service failure doesn't interfere with the order, and the failure is visible through the project's established mechanism. Test addresses and a sending stub are suitable for these checks; real customers shouldn't be part of the experiment.
I'd also ask which checks actually ran and what remains unverified. A persuasive explanation doesn't mean the implementation has been exercised through the relevant scenarios. If something couldn't be checked, that's useful information for the next step.
This task leaves more than a new email behind. We've learned where the relevant order stage ends, how notifications are supposed to be sent, and which constraints matter. That knowledge belongs alongside the code or in the project documentation. Next time, we won't have to hunt for it again through a long conversation.
I like that you can start with perfectly ordinary words. You don't need to know the solution's architecture immediately or compose an ideal prompt. Explain the outcome, name the important constraints, and assign a first step you can verify. The conversation then becomes more precise: specific code locations, behavioral questions, and a small plan emerge.
“Add an email” gradually becomes a task both of you understand the same way. That shared understanding is what you can rely on when the assistant brings back the changes.
The next question comes just before release: is reasonable-looking code and a set of passing checks enough? In the next article, we'll look at how to judge whether the result is ready to ship.
Top comments (3)
This is great advice. The part about asking the assistant to separate what it found in the code from what it is assuming is the step most people skip, and that is where bad code comes from. One thing I would add: give the assistant your acceptance criteria first, so it can check its own work before showing you. Thanks for writing this.
Thanks! That’s a useful addition. In the order-email example, I’d give those criteria upfront: send the notification at the agreed stage, handle repeated events without sending extra emails, and keep order processing working if email delivery fails.
I’d also ask the assistant to show how it checked each criterion and flag anything it couldn’t verify. That makes the self-check much easier to review.
Exactly. The showing-its-work part is the real win. One thing I started doing: keep the acceptance criteria in a short checklist file in the repo itself, so every session starts from the same bar instead of retyping it. Then the assistant's self-check against that file becomes something you can diff over time.