What Should Programmers Still Practice in the AI Era?
This is the twelfth article in Programmer Craft in the AI Era, and it is time to close the series.
We started from “program = algorithm + data structure”, then moved through processes, threads, coroutines, module communication, API contracts, databases, caches, queues, high concurrency, testing, observability, and refactoring. After walking this full loop, the point is still the same: AI can help you write code, but it will not carry the consequences of the system for you.
What programmers need to practice over the long term is not memorizing more framework names. It is building stable engineering judgment: how to split a problem, how state flows, where boundaries belong, how cost is estimated, how results are verified, and how risk is contained.
Do Not Treat AI as a Junior Programmer
Many people describe AI as a very fast junior programmer. That metaphor is half right and half wrong.
The right half is that AI really does need clear tasks, context, and acceptance criteria. Give it a vague request, and it fills in the blanks by probability. Give it clear boundaries, and it becomes much more stable.
The wrong half is that a junior programmer gradually develops responsibility inside a project. They learn who is affected after a piece of code goes live. AI has no such responsibility. It does not know what it feels like when a production alert rings at midnight. It does not know how to compensate users after an incorrect charge.
So the programmer’s role is not “let AI finish it for me”. It is “shape the problem so AI can work correctly”.
That shape usually includes:
clear inputs and outputs
stable data structures
explicit state machines
verifiable test cases
controlled side effects
observable runtime evidence
rollback-capable release paths
These things sound ordinary, but they are engineering.
Long-Term Skill 1: Problem Modeling
Before writing code, model the problem.
Modeling is not about drawing pretty diagrams. It is about compressing messy real-world concepts into objects, relationships, states, and constraints that software can handle.
Take this requirement: “A user submits a task, the system executes it asynchronously, and returns a result.” That sentence is too coarse. Real modeling asks:
What states can a task have?
How does it move between states?
Can a task be cancelled?
How do we handle duplicate submissions?
Is execution failure terminal or retryable?
Does the result have versions?
Can users see other users' tasks?
Does execution produce a charge?
Does charging happen on submission, before execution, or after success?
Very quickly, you get a state machine:
created -> queued -> running -> succeeded
|-- failed_retryable -> queued
|-- failed_final
`-- cancelled
That state machine is far more useful than a one-line requirement.
It guides database fields, API error codes, worker idempotency, test cases, frontend display, and alerting rules.
Before asking AI to write code, first ask it to model with you:
Do not write code yet.
Model this requirement as entities, fields, a state machine, transition constraints, and failure scenarios.
Point out which transitions need transaction protection and which operations must be idempotent.
AI becomes useful only when you can model the problem.
Long-Term Skill 2: Complexity Judgment
Complexity is not just symbolic notation from an algorithms class.
In real systems, complexity determines whether a feature survives real data.
Look at this AI-written code:
for user in users:
orders = await db.get_orders(user.id)
for order in orders:
items = await db.get_items(order.id)
result.append(build_row(user, order, items))
It looks natural, but it may be a disaster.
If there are 10,000 users, 20 orders per user, and 5 items per order, this code can issue a huge number of database queries. The problem is not Python syntax. The problem is complexity.
You need to convert it into a batch-oriented shape:
fetch the user set once
fetch orders by user_id in batch
fetch items by order_id in batch
join them in memory with hash maps
Pseudo-code:
orders_by_user = group_by(await db.get_orders_by_user_ids(user_ids), key="user_id")
items_by_order = group_by(await db.get_items_by_order_ids(order_ids), key="order_id")
for user in users:
for order in orders_by_user.get(user.id, []):
items = items_by_order.get(order.id, [])
result.append(build_row(user, order, items))
The knowledge behind this is basic: hash tables, batch queries, time complexity, and I/O count.
AI can write the second version, but only if you know why the first version is wrong.
Long-Term Skill 3: Sensitivity to State
Many production bugs are state bugs.
Duplicate requests, concurrent submissions, task retries, cache expiration, transaction rollback, and repeated message delivery all eventually become the same question: what state is the system in now?
AI often writes only the happy path:
job = await repository.get_job(job_id)
result = await provider.call(job.payload)
await repository.save_result(job_id, result)
await repository.mark_succeeded(job_id)
Engineering code needs state protection:
job = await repository.lock_job(job_id)
if job.status not in {"queued", "failed_retryable"}:
return SkipReason("invalid_status")
await repository.mark_running(job_id)
try:
result = await provider.call(job.payload)
except TimeoutError:
await repository.mark_retryable_failed(job_id)
raise
await repository.save_result_once(job_id, result)
await repository.mark_succeeded(job_id)
This is where transactions, locks, idempotency, and state transitions appear.
A state-sensitive programmer naturally asks:
Can this operation run twice?
What happens if two people click at the same time?
What happens if the worker restarts halfway through?
What if the external API succeeds but the local database write fails?
What if the message queue delivers the same message again?
When these questions are asked, AI-generated code moves from demo code toward production code.
Long-Term Skill 4: A Sense of Boundaries
A sense of boundaries is one of the most important programmer virtues.
What should a module know, and what should it not know? What should a function do, and what should it refuse to do? What should an API expose, and what should remain private?
Without boundaries, a system becomes this:
API handlers assemble SQL directly
database layers decide user permissions
tool-call layers quietly charge users
logs contain complete user inputs
frontends depend on temporary backend fields
scripts directly mutate production state
All of this may run in the short term. All of it becomes debt in the long term.
Boundaries are not about ceremonial layering. They are about giving change a place to go.
When permission rules change, change the permission module. When a third-party provider changes, change the provider client. When billing strategy changes, change billing. When the frontend display changes, do not affect database transactions.
Prompts to AI should be concrete:
The API handler only parses requests, calls the use case, and returns responses.
Business rules belong in the domain layer and should be pure functions where possible.
Database access goes through repositories; do not assemble queries inside handlers.
External services go through provider clients with timeouts and error mapping.
Billing, audit, and analytics are separate side effects, not scattered through business branches.
The stronger your sense of boundaries, the less likely AI-generated code is to grow chaotically.
Long-Term Skill 5: Feedback Loops
Engineering does not end when the code is written.
After code goes live, you need to know how users use it, where the system is slow, where errors happen, which inputs are common, and which features nobody touches. All of that should feed back into the next round of design.
Feedback loops include:
test feedback: whether code matches expectations
build feedback: whether dependencies, types, and formatting are stable
production feedback: whether error rate, latency, and resource usage are normal
business feedback: whether users actually complete their goals
team feedback: whether others can understand and maintain it
AI coding increases output speed. Without feedback loops, errors accumulate faster too.
A practical rhythm is:
generate in small steps
test in small steps
review in small steps
release in small steps
watch metrics before continuing
Do not ask AI to generate an entire large system at once and then try to find problems in 5,000 lines of code. That is not efficiency. It is deferred risk.
Long-Term Skill 6: Product Judgment
Programmers cannot only judge technical feasibility.
In the AI era, feature development is cheaper, so the number of wrong features can also increase. In the past, many ideas were naturally filtered out because development was expensive. Now a single prompt can generate something that looks complete, which makes product judgment even more important.
Product judgment does not mean “become a product manager”. It is part of engineering decision-making.
You need to ask:
Whose problem does this feature solve?
What is the shortest path for the user to complete the task?
Will this feature increase maintenance cost?
Is there a smaller version we can validate first?
Is the data accurate enough, or might users over-trust it?
When it fails, can users understand what happened?
Many technical debts do not begin with bad code. They begin with unclear requirements.
AI can reduce development cost, but it cannot prove that a requirement has value. A programmer who can write code but lacks product judgment will create useless systems faster.
Long-Term Skill 7: Expression and Collaboration
AI coding makes expression more important.
Requirements, designs, and PR descriptions used to be written for people. Now they are also context for AI. The clearer the expression, the more likely AI is to do the right thing. The vaguer the expression, the more AI will invent.
Good expression is not about length. It is about structure:
Background: why this exists.
Goal: what counts as done.
Non-goals: what is explicitly out of scope.
Inputs and outputs: where data comes from and where it goes.
Constraints: performance, permission, compatibility, cost.
Failure scenarios: cases that must be handled.
Acceptance: tests, metrics, examples, and rollback.
Team collaboration is the same.
After AI generates code, you still need to explain why it was done this way. Do not throw a large chunk of generated code into review and say, “AI wrote it.” Once code enters the repository, it is a team asset. The responsibility is still human.
Long-Term Skill 8: Knowing When Not to Use AI
AI is powerful, but not every scenario is suitable for direct code generation.
Be especially careful with:
permission and security boundaries
charging, settlement, and financial records
data deletion and irreversible operations
database migrations
high-concurrency core paths
complex concurrency control
privacy and compliance logic
cross-system state repair scripts
This does not mean AI cannot participate. It means AI-generated work cannot be accepted and executed without review.
Better uses include:
ask AI to list risks
ask AI to generate tests
ask AI to explain migration impact
ask AI to do read-only analysis
ask AI to draft, then review line by line
The stronger the tool, the more boundaries it needs.
A Long-Term Practice List
If I kept only one checklist, I would practice these things for the long term:
1. When you see a requirement, draw the state machine first.
2. When you see a loop, estimate data volume and complexity first.
3. When you see a database, ask about indexes, transactions, and locks.
4. When you see an external call, ask about timeouts, retries, and idempotency.
5. When you see an API, ask about schema, error codes, and compatibility.
6. When you see an async task, ask about repeated delivery and recovery points.
7. When you see a cache, ask about invalidation, consistency, and penetration.
8. When you see AI-generated code, ask about tests and observability.
9. When you see a large change, ask about canary release, rollback, and data repair.
10. When you see a new feature, ask whether anyone really needs it.
These are not tied to any single framework. They are engineering habits that survive across languages, platforms, and eras.
A General Prompt for AI
At the end of this series, here is a prompt worth keeping:
You are not responsible only for writing code.
Before implementation, help me complete the engineering design:
1. Split the requirement into entities, states, inputs, outputs, and constraints.
2. Explain the core data structures and algorithmic complexity.
3. Explain module boundaries, dependency direction, and where side effects belong.
4. Explain database tables, indexes, transactions, and locks.
5. Explain API schema, error codes, idempotency, and version compatibility.
6. Explain caches, queues, async tasks, and failure recovery.
7. Explain the testing plan: unit tests, integration tests, contract tests, regression tests.
8. Explain observability: logs, metrics, traces, audit records, and business events.
9. Explain the release strategy: canary, rollback, data repair, and monitoring.
10. Mark anything uncertain instead of assuming it silently.
After the design is confirmed, generate code step by step. For each step, explain the changed scope and verification method.
This prompt cannot guarantee that AI will always be right, but it forces the conversation back to engineering.
Programmers Are Not Obsolete. The Threshold Has Changed.
In the AI era, writing a few lines of code will become easier and easier.
But the threshold for building systems has not disappeared. State can still be wrong. Transactions can still conflict. Caches can still become inconsistent. Queues can still deliver duplicates. APIs still need compatibility. Users still make mistakes. Production still sends alerts.
The difference is that programmers used to spend a lot of time typing code. Now more time will be spent defining problems, organizing context, reviewing results, verifying behavior, and owning consequences.
That is not a step backward. In a sense, the core part of programming work is finally becoming visible.
In the long run, the valuable programmer is not the one who remembers a few more APIs than AI, nor the one who can type faster than AI.
It is the one who knows what should be generated, what should be written by hand, what must be verified, what must not go live, what should be deleted, and what is worth maintaining for years.
AI amplifies a programmer’s ability. It also amplifies a programmer’s blind spots.
That is exactly why craft matters even more.
Follow ZiCode on WeChat
If this post was useful, you can follow later updates on WeChat as well.
X / Twitter
Follow @ax2_zicode
Faster technical notes, short thoughts, and new-post alerts are posted on X.