Whether people like admitting it or not, AI has already become a significant part of software development. Some developers use it openly, some use it only for small tasks, and some quietly generate half of a pull request before joining a meeting and talking as if every line came to them during a long night of architectural enlightenment. To me, there is no reason to be embarrassed about using it. In contrast, I use it, I promote its usage. After all, software development has never been a competition about who types the largest number of characters without receiving help from anywhere else. It is about solving a problem in given constraints.
We have always used tools and knowledge created by other people. We use frameworks instead of building every low-level component ourselves, install libraries written by developers we have never met, search documentation, inspect open-source projects and copy small pieces of code from Stack Overflow or a random dude’s blog. IDEs complete our code, refactoring tools move code around, compilers optimise what we write for decades. AI is another step in this long journey, even though it is clearly a big one. Sometimes, it is more unpredictable than most of the tools that came before it. Frankly speaking, it can do some types of work remarkably well. It can generate repetitive mappings, prepare an initial implementation, explain unfamiliar code, convert one format into another, write test scaffolding and remove a considerable amount of dull manual work.
Here in this post, I want to talk about what are the main important points when we use AI to develop code and what can we do, depending on my own experience.
Having Control and Its Importance
Control is the central issue in AI-assisted development. Without it, all the speed and convenience AI gives us can start working in the opposite direction, because receiving code quickly is not the same thing as making progress. A large amount of generated code may create the feeling that a difficult task is nearly finished, although the real work of understanding, validating and fitting it into the system has only just started. We should not confuse visible output with completed engineering.
When developers write code manually, understanding generally grows together with the implementation. You write one function, discover that an assumption was wrong, adjust the design, run the application, add another part and gradually form a mental picture of the whole solution. This process doesn’t guarantee good code, of course; human beings were producing terrible software long before artificial intelligence arrived, and some of it was written very carefully. Nevertheless, manual work creates a certain friction, and that friction gives us time to think. However, AI removes much of this friction. You can describe a feature and receive several hundred lines before finishing your coffee, complete with respectable names, clean formatting and a confident explanation about why the solution follows best practices.
Losing control doesn’t happens slowly. You may find yourself in a code base that you don’t recognise anymore within the matter of minutes. You accept one change you only partly understand, build another change on top of it, and then ask AI to repair a problem introduced by the previous change. After a few iterations you end up with a piece of code that nobody is entirely knows.
1. Plan Before Prompting
The most important part of AI-assisted coding happens before asking AI to write any code. Planning is not a chore that can be skipped because the machine appears capable of filling in the missing details. In fact, the more capable the tool becomes, the more important planning becomes, because a vague instruction can now produce a very large and convincing result. With traditional development, a weak plan can waste time; with AI-assisted development, a weak plan can waste time at industrial speed.
Planning doesn’t mean writing a fifty-page design document for every endpoint or drawing diagrams because somebody once said that senior developers draw diagrams. It means deciding what you are trying to achieve, understanding where the change belongs, separating the work into manageable parts and knowing how you will judge whether the result is correct. The plan can be a simple md document, a few notes or a clearly written sequence of tasks. Its value does not come from its size, but from the decisions it makes visible before those decisions become code.
Do not deviate from the basics of engineering.
a. Define the problem
The first thing to clarify is the actual problem, as a very basic engineering practice. Many AI prompts begin with a solution rather than a problem, which quietly delegates one of the most important engineering decisions to the model. A developer asks AI to add Redis or create a new microservice without first explaining what is wrong with the current system, for example. These may all be reasonable solutions, but they are still solutions, and the underlying problem may be much smaller or completely different.
Before prompting, define what should be different after the work is completed, in other words, what you expect as outcome. Which behaviour must change, which behaviour must remain exactly the same, what is inside the scope and what should remain outside it etc. You should also decide what success and failure look like. “The code compiles” or “the agent says the task is complete” are very weak definitions of success. A successful change may mean that an operation finishes within a certain time, preserves compatibility with existing clients, remains safe to retry or fails without damaging data.
b. Define the scope
The next part of planning is understanding the territory in which the change will happen. Which components are involved, where does the data enter the system, where does it go, and which public interfaces may be affected? Are there background jobs, scheduled processes or consumers in another repository depending on the current behaviour? Does the database change need to remain compatible with an older version of the application during deployment? These details often decide whether a locally reasonable implementation is safe for the wider system.
When these questions are unanswered, AI doesn’t somehow discover the missing context through intuition. It sees the information you provide, fills the gaps with plausible assumptions and continues from there. Sometimes those assumptions are excellent and save a considerable amount of work. Sometimes they are completely wrong, but expressed so clearly and professionally that they survive a quick review. This is why decomposition is at the heart of planning.
c. Keep tasks smaller and “not open-ended”
Never give AI a lump sum of work if you want to remain in control since prompts such as “implement this feature” or “refactor this service” leave the boundaries, order and internal decisions to the model. The result may contain a new interface or an additional dependency.Every individual decision may look defendable, yet reviewing all of them together becomes difficult because too much has happened in one step.
Break the work into small tasks with one main purpose and one observable result. Suppose you are adding a new operation to an existing API. You might first analyse the current request flow and identify the components involved, then define the request and response contracts, establish the business rules, describe the edge cases, prepare the tests, update the domain logic, change persistence and finally expose the operation through the API. Each task should answer a simple thing to do. When a task has no clear completion condition, it is generally still too broad. “Improve the service” is not a useful unit of work, while “remove the duplicate query without changing the public response” is much closer to something that can be generated, reviewed and verified safely.
Uncertainty should be made visible in the plan as well. There may be an area of the codebase you do not understand, an external API whose behaviour is unclear or a business rule with two reasonable interpretations. Do not hide these uncertainties inside a large implementation prompt and hope that the model chooses correctly. Turn them into investigation tasks, use AI to trace the code or compare the interpretations, and then verify the answer before allowing implementation to begin. This is actually one of the areas where AI shines. It can inspect several files, trace a call flow, identify where a rule is enforced or summarise the likely effects of a change, giving the developer a starting point much faster than reading everything manually. However, the output is still a hypothesis, not a court decision. A mistaken assumption discovered in an analysis paragraph is cheap to correct, while the same mistake spread across twelve generated files is much more expensive.
d. Own the Engineering Judgement
AI can generate implementation, propose architectures and compare design options, but engineering requires context that any AI model can have. It may not know the team’s operational limits, the product roadmap, past architectural failures or the business consequences of a technically elegant decision. AI can support these choices, but it should not make them silently.
This is especially important with complexity. AI often adds abstractions, dependencies and extension points for requirements that do not yet exist, and every one of them creates maintenance cost. The simplest design is not always the best, but complexity should be introduced deliberately and in response to a real need. AI may do much of the implementation, but engineering judgement must remain human.
2. Have an Understanding
Planning gives direction, but the developer still needs to understand what is being changed. You do not need to know every corner of a large codebase, but you should understand enough of the relevant area to judge whether a proposed change belongs there. AI can help by tracing call flows, explaining classes, locating implementations or summarising how errors move through the system. This can be more useful than asking it to write code immediately. Still, its explanation must be checked, because it may not see runtime configuration, generated code, external consumers or behaviour outside the supplied context. That is how code can be locally correct but globally wrong: a function may return the right value while bypassing validation, breaking a transaction or introducing stale data elsewhere.
After each generated step, inspect the files, dependencies, configuration, public behaviour and tests. Do not rely on the confident summary beside the code. AI can explain why its solution is sensible, but the real evidence is in the diff, the tests and the running system. You should be able to defend every meaningful decision without reopening the original conversation.
AI can generate code much faster than you can review it. When the amount of output begins to exceed your understanding, stop, reduce the scope and return to the last state you understood clearly. Continuing beyond that point only increases your responsibility without increasing your control.
AI-generated code should be treated as potentially untrusted work that you have chosen to adopt.
3. Accountability Has Not Changed
Do not forget that you are the developer pushing the code into the codebase. It doesn’t matter whether every character was typed manually, copied from somewhere, accepted through autocomplete or generated by an AI agent. Once you review, approve and merge the change, it becomes part of the system your team owns. The origin of the code may remain interesting, but it does not reduce the responsibility attached to the decision.
This problem didn’t begin with AI. Developers were copying code from Stack Overflow, GitHub repositories, or “a random dude’s blogs” long before large language models entered the game. Sometimes the copied code was fully understood and adapted carefully, and sometimes a variable name was changed after copy-paste and everybody moved on with their lives. The quality of the decision depended on the developer, not on the existence of the source. If copied code caused a production incident, the author of the blog post was not expected to join the incident call. A Stack Overflow user wasn’t going to explain the business impact to the customer, repair the damaged data or prepare a rollback before morning. Responsibility remained with the developer and the team who decided to place that code inside their system. AI changes the volume and speed of this old problem, but it doesn’t change the principle.
In fact, the increased speed makes the accountability issue more serious. Copying a small snippet created a limited review problem, whereas an AI coding agent can touch twenty files, modify configuration, add a library and generate tests in a few minutes. A much larger amount of unverified work can enter the review process before anybody pauses to ask whether the direction was sensible. The convenience of generation doesn’t make acceptance less significant.
The AI will not explain why customer data was exposed, why the migration cannot be rolled back or why a supposedly reusable abstraction has made every simple change more difficult. You and your team will carry those consequences, even when most of the initial implementation was produced by a machine. For this reason, AI-generated code should be treated as potentially untrusted work that you have chosen to adopt. Once it has been understood, reviewed, tested and accepted, the distinction between human-written and AI-written code becomes much less important. The code is yours when you push it, and the responsibility arrives with it. That was true when we copied answers from forums, and it remains true now that the forum can answer back.
4. Practical Ways to Work With AI
In this section, I will talk about some practical tips and tricks that we can use when we are developing using AI.
a. Set Standards Before Writing Code
Before asking AI to implement anything, decide what acceptable code looks like. This includes naming, formatting and file organisation, but the important standards normally go beyond style and cover architectural boundaries, dependency rules, security expectations, error handling, logging, performance, compatibility and testing. Should the domain layer remain independent from infrastructure, does the project use exceptions or result objects for business failures, and can this component introduce a new external package? These are small questions individually, but together they define whether code belongs in a particular system.
If these rules are not stated, AI will fill the gaps with general assumptions, or even hallucinations. Those assumptions may be perfectly reasonable in another project and completely wrong in yours. Telling the model to write “clean code” doesn’t solve this problem, because clean code is contextual. A solution can look tidy and modern in isolation while ignoring the conventions and compromises that keep an established system.
b. Provide Examples and Counterexamples
Examples make intention concrete. Show valid inputs and expected outputs, but also show invalid inputs and the failures they should produce. Boundary cases are particularly valuable because many defects live just outside the happy path: an empty value, the maximum permitted length, one character beyond it or a value containing an unusual character. Counterexamples show what the system must not do and give the model a clearer shape of the boundary.
The same approach applies beyond validation. If an API must preserve backward compatibility, show the current and expected response; if generated code should follow a local convention, provide an example from the repository. If one failure should be retried and another rejected immediately, demonstrate both situations. Examples reveal intent much more effectively than vague instructions such as “handle errors properly.”
Still, examples should support the specification rather than replace it. A model can imitate a handful of examples without understanding the broader rule, just as a developer can write code that passes three tests while missing the actual requirement. The strongest prompt combines a clear rule with representative positive and negative cases. Tests make these examples executable, which gives them more value than prose alone.
c. Give Every Prompt a Clear Purpose
Do not talk to or fight with the toaster as though it has feelings, intentions or professional pride. AI communicates in a human-like way, and it can apologise, praise your correction and announce that it now understands everything perfectly. Politeness is harmless, but fluent conversation should not be confused with understanding or accountability. The tool is still producing output from the context and instructions it has received.
Every prompt should have a clear purpose. Ask the model to trace one flow, identify assumptions, compare two approaches, produce tests for defined behaviour or implement one bounded change. Know what kind of result you expect before sending the request, because otherwise it becomes difficult to recognise when the answer has drifted away from the task. A conversation without a purpose may still sound intelligent while achieving very little.
When the output is wrong, arguing emotionally with the machine rarely helps. Identify what was misunderstood, correct the context, narrow the task, state the missing constraint or begin again with a cleaner conversation. Long AI conversations can collect their own technical debt: old assumptions remain in the context, later corrections compete with earlier instructions and the model continues preserving a direction that should have been abandoned several prompts ago. Sometimes another correction is useful, but sometimes starting again is the more sensible option.
d. Review the Diff and Keep Changes Reversible
When an AI agent modifies a repository, it generally provides a helpful summary. It may claim that validation was added, error handling was improved, tests were introduced and backward compatibility was preserved. Read the summary, but inspect the actual code, including configuration, migrations, permissions and dependencies. The story around the change may be convincing, but the diff is where the decisions live.
Look for modifications outside the original scope and watch for unrelated formatting or refactoring that hides behavioural changes. AI often tries to improve nearby code while completing a task, renaming variables, reorganising methods or modernising an API nobody asked it to touch. Some of these changes may be reasonable, but they increase the review surface and make the original purpose harder to see. Ask for focused changes, and move useful refactoring into a separate task where it can be reviewed independently.
Small tasks should also produce small and reversible changes. Use focused commits, clear checkpoints and feature flags where they make sense, and avoid combining a feature, a package upgrade, a schema migration and an unrelated refactoring in the same generated change. When too many concerns move together, it becomes difficult to know what should be reverted after a failure. Reversibility is not merely a Git convenience; it is part of maintaining control over the work.
Database changes deserve particular care because a migration that works perfectly in development may still fail during a real deployment where old and new application versions briefly run together. A destructive change may become impossible to reverse after production data has passed through it. Thinking about rollback does not mean expecting disaster; it means recognising that software sometimes behaves differently outside the comfortable world of the local environment. A change that cannot be safely reversed needs more planning, not more confidence.
e. Verify Dependencies and APIs
AI can produce code that looks completely plausible while using methods that do not exist, configuration from another version or a library that is unnecessary for the task. A convincing method name and correct-looking syntax are not proof of anything. Verify external claims through the official documentation and the versions actually used by the project, then compile and run the code. The model’s confidence is not a substitute for the behaviour of the real library.
A suggested dependency should be checked for existence, maintenance, compatibility, licensing and known security problems. More importantly, ask whether another dependency is needed at all. A package introduced to save twenty lines becomes part of the system for years and must be restored in builds, upgraded, monitored and understood by future developers. The model receives the immediate convenience, while the team receives the long-term ownership.
The same caution applies to cloud services, infrastructure configuration and external APIs. A generated example may be syntactically correct while being operationally expensive, insecure or incompatible with the environment. It may ignore quotas, authentication, retries or the behaviour of partial failures. Plausible code may not work, and working code may still be unsuitable for the system in which it is placed.
f. Do Not Let AI Create Unnecessary Complexity
AI has a habit of making solutions flexible, reusable and prepared for possible future requirements. These qualities sound positive, but they can become an excuse for building complexity the project has not earned. A single implementation may receive an interface and a factory, while a straightforward workflow becomes a configurable pipeline. The result can look impressive even though a much simpler solution would have been easier to understand and maintain. Ask what each abstraction contributes today and whether a simpler implementation would remain clear. An existing utility may be sufficient, while a new generic framework may create more navigation, configuration and testing than value. You can tell AI explicitly not to introduce abstractions unless they remove real duplication or satisfy a stated requirement. Asking for the smallest change compatible with the current architecture often produces better results than asking for something “enterprise-ready.”
Of course, simplicity should not become another form of dogma. Sometimes a proper abstraction prevents duplicated behaviour, and sometimes a more structured design is safer than a quick local fix. The issue is not whether the solution contains patterns, layers or interfaces; the issue is whether they solve a real problem. Sophistication is not a reliable measure of quality, and generated complexity is still complexity somebody will have to maintain.
g. Lean Heavily on Testing
AI makes writing code easier, and naturally it can make producing defects easier as well. Testing provides boundaries that fast generation doesn’t create by itself. Unit tests define local behaviour, integration tests show whether components continue to work together, regression tests protect existing behaviour and end-to-end tests verify important paths that cross several boundaries. Which types are needed should be decided during planning rather than after the implementation appears complete.
TDD fits AI-assisted development particularly well because expected behaviour is defined before the implementation is produced. Instead of asking the machine to build a feature and then explain what it does, establish the behaviour through tests and request the smallest implementation that satisfies them. This changes the balance of the interaction: the generated code is no longer inventing the feature while implementing it. It must fit inside boundaries that already exist.
However, AI-generated tests require the same scrutiny as production code. A model can repeat the same misunderstanding in both places, writing an incorrect implementation and then creating tests that confirm the incorrect assumption. It can mock so much of the system that the tests prove almost nothing, or assert implementation details instead of business behaviour. A green test suite is useful only when the tests represent the real requirement and would fail for meaningful defects.
Review tests against the specification rather than against the generated implementation. Check the failure paths, boundary conditions and interactions that cannot be proven through isolated unit tests. Then run everything, because a test suite that exists only inside the response window provides comfort rather than evidence. Static analysis, linters, type checking and build pipelines add further boundaries, and while none of them proves correctness, together they reduce the space in which obvious mistakes can survive.
5. Security Needs Its Own Attention
Security rules do not disappear because the code came from an AI assistant. In some ways, AI-assisted development makes ordinary security discipline easier to forget because sharing context and accepting generated output feel effortless. The chat interface creates an informal atmosphere, and informal environments often encourage people to paste more than they should. Security must therefore apply both to the information given to the model and to the code received from it.
a. Protect Sensitive Context
AI normally needs context to be useful. It may need a function, an error message, a configuration section or part of a log, and the easiest reaction is often to paste everything and allow the model to work out what matters. However, a large log file may contain personal information far away from the few lines currently being investigated, while a configuration file may hide a secret near the bottom. Share the smallest amount of context required for the task, remove sensitive values and replace real customer data with synthetic examples. There is rarely a good reason to paste a complete production dump when a short, sanitised extract is enough. Limiting context is not only good prompting discipline; it is a basic security control. You might even have a script that masks the sensitive data first and then you might give it to AI.
b. Apply Normal Security Review
Generated code must pass the same security standards as code written by a person. Check input validation, authentication, authorisation, secret handling, permissions, file access, database queries and external requests. Verify that one user cannot access another user’s data, and inspect how errors are returned or logged. AI is perfectly capable of generating insecure code that looks clean, modern and well documented.
Error handling can create similar risks. Generated code may expose internal details through API responses, write tokens or personal data into logs, retry an operation that is unsafe to repeat or trust data received from an external source. Ask AI to help identify trust boundaries, abuse cases and suspicious patterns, but do not allow the same model that produced the code to become the only security reviewer. It can offer another perspective, not an independent guarantee.
Use the normal security processes available to the project. Human review, secret scanning, dependency scanning, static analysis, dynamic testing and specialist involvement still matter. AI may accelerate some of these activities, but it doesn’t absorb the responsibility attached to them. Security remains an engineering concern from the first plan to the final deployment.
c. Treat Dependencies as a Security Decision
A new dependency is not only a development convenience; it becomes another part of the supply chain the system must trust. Suggested packages should be verified for existence, maintenance and suitability, while known vulnerabilities, similar-looking package names and excessive runtime permissions should all be considered. The fact that a package solves the immediate problem doesn’t mean that bringing it into the system is automatically a good trade.
This principle applies beyond application libraries. Generated container images, CI workflows, deployment templates and infrastructure modules can execute with significant permissions and influence the whole delivery process. A convincing configuration file should not escape scrutiny merely because it resembles something commonly seen in documentation. Security should shape planning, standards, implementation and review, rather than appearing as a final gate where somebody else is expected to clean up the work.
Use the tool, but remain as an engineer
Conclusion
AI can perform a considerable amount of heavy lifting, and there is no reason to pretend otherwise. It can remove repetitive work, accelerate exploration, explain unfamiliar areas and allow developers to reach an initial solution much faster than before. Used properly, it can make software development more productive and sometimes improve the quality of the discussion around a problem. Used carelessly, it can produce a large amount of plausible complexity before anybody has clearly decided what should be built.
The difference comes from how the developer approaches the work. Plan before prompting, define the outcome and boundaries, divide the change into small tasks and decide how each part will be verified. Set standards, provide examples, keep prompts purposeful and use AI for analysis as well as generation. Review the actual diff, keep changes reversible, verify external claims and reject complexity that the project doesn’t need.
Most importantly, remember who is pushing the code. The machine may write a function, a feature or most of a pull request, but it doesn’t own the system and it will not carry the consequences of a poor decision. That responsibility still belongs to the developer and the team, just as it did when the copied code came from Stack Overflow or a random blog. AI has changed the speed at which software can be produced, but it has not changed who must understand, defend and maintain the result.
Suleyman Cabir Ataman, PhD

Leave a Reply