AI Can Write Code, But You Still Need to Know What a Program Is
The column Programmer Craft in the AI Era is not about which AI tool completes code faster. Tools change. Models change. But the basic laws of running software do not change. The person writing software still has to understand how a system is decomposed, how a problem is described, and how generated output should be reviewed.
This first article starts with an old sentence: programs = algorithms + data structures. It has been in textbooks for decades, but it matters even more in the age of AI-assisted programming. AI is very good at turning a sentence into code. But if you have not turned the problem into algorithms and data structures, what it produces is often only code that appears to run.
The Old Sentence Has Not Expired
Many people first heard “programs = algorithms + data structures” in a university classroom, in a C textbook, or in an old Pascal-era book.
It sounds plain, even a little old-fashioned. Today we build web services, mobile apps, recommendation systems, data platforms, and agent toolchains. It can feel far away from “algorithm exercises” and “linked lists, trees, and graphs”.
But once you take apart a real system, that sentence is still holding up the foundation.
In an order system, the algorithms are not sorting exercises. They are price calculation, inventory deduction, coupon stacking, risk checks, and state transitions. The data structures are not hand-written red-black trees. They are order tables, state machines, indexes, event logs, cache keys, and message payloads.
In a search system, the algorithms are not textbook binary search. They are recall, filtering, ranking, fusion, deduplication, and pagination. The data structures are not abstract arrays and trees. They are inverted indexes, vector indexes, document structures, feature fields, and scored results.
In an AI agent, the algorithm is not simply “let the model think”. It is task decomposition, tool selection, failure retry, context trimming, and result validation. The data structures are not the model’s internal tokens. They are message queues, tool schemas, execution records, state snapshots, and recoverable intermediate results.
So “programs = algorithms + data structures” does not mean programmers must solve coding interview puzzles every day. It means this: a program is not a pile of statements; it is how data is organized and how rules act on that data.
AI can help you write statements. It cannot decide for you what the data inside your system should look like.
Pick Up the Old Craft Again
Simply saying “algorithms and data structures are important” is almost the same as saying nothing. For people who already work as developers and now use AI heavily, the following concepts are worth revisiting. Not to go back to grinding algorithm problems, but to make the right structure naturally come to mind when reading a requirement.
| Concept | Common shape in engineering | What AI easily gets wrong if you do not understand it |
|---|---|---|
| Array / dynamic array | Paginated results, batch buffers, time-series points | Frequent head insertion or deletion, hiding O(n) movement cost |
| Hash table | Deduplication, cache, index, aggregation by ID | Unstable key design, unbounded memory growth, ignoring collisions and expiry |
| Stack | Expression parsing, recursive expansion, call chain, undo | Recursion without depth protection, failure paths that cannot roll back |
| Queue / priority queue | Task scheduling, message consumption, rate limiting, delayed retry | Mixing FIFO and priority semantics, putting retry tasks back in the wrong position |
| Tree | Comments, organizations, permission inheritance, route matching | Recursive full-tree queries, deep-node performance and stack-depth problems |
| Graph | Dependency analysis, workflows, recommendation relations, service call chains | No cycle detection, wrong topological order, uncontrolled cascading calls |
| Heap | Top K, timeout management, scheduled tasks, approximate leaderboards | Full sorting instead of local selection, turning O(n log k) into O(n log n) |
| Bloom filter | Blacklists, cache-penetration defense, large-scale dedup precheck | Treating “may exist” as “must exist” and getting false-positive semantics wrong |
| LRU / LFU | Local cache, hot data, connection-pool eviction | Using only a dict, with no capacity limit and no access-order maintenance |
| Inverted index | Search, tag filters, log retrieval | Scanning all text on every request and filtering with string contains |
| State machine | Orders, tasks, approvals, payments, deployment pipelines | Arbitrary state mutation, no transition constraints, no protection against invalid states |
This article will not yet expand on red-black trees, B+ trees, skip lists, or LSM trees. Those lower-level structures matter, but the first step is to make modeling clear. When later articles discuss databases and high concurrency, we can talk about why B+ trees affect indexes, why LSM trees affect writes and compaction, and why skip lists often appear in ordered in-memory structures.
In AI-assisted programming, the point is not to hand-write a perfect heap. The point is to see a requirement like “find the 100 slowest requests in the last five minutes” and immediately realize that you should not let AI simply write sort(all_requests). The data stream may be large. A better approach is to process it as a stream and maintain a min-heap of size 100; each incoming request only compares with the heap top. Memory complexity is O(k), not O(n).
That kind of judgment is where professional knowledge is most useful today.
The Step AI Most Easily Skips
When asking AI to write code, we often say something like this:
Write a task scheduling system that supports creating tasks, executing tasks,
and checking task status.
The model can usually produce APIs, a database table, and several pieces of execution logic very quickly. It looks complete, but the trouble is often hidden later.
What states can a task have? Are pending, running, success, and failed enough? Do we need cancelled, timeout, or retrying? After a task fails, should it retry immediately or wait? Where is the retry count stored? If the process dies halfway through execution, how is the state recovered? Can the same task run twice concurrently? Does the state update need a transaction?
If these questions are not thought through first, AI-generated code will silently choose a set of hidden rules. Hidden rules are dangerous because they have not been confirmed by product, engineering, operations, and testing.
I now prefer asking AI to decompose the problem before writing the implementation:
Do not write code yet.
Break the "task scheduling system" into:
1. Core entities and fields
2. State machine
3. State transition rules
4. Concurrent execution constraints
5. Failure and retry strategy
6. Operations that require transaction protection
7. Reproducible test cases
This prompt is not fancy, but it works. It pulls AI back from “code generator” to “design assistant”. Code generation only becomes meaningful after the data structures and algorithmic rules are clear.
Push one step further: a task scheduling system should first land as a state-machine table.
| Current state | Allowed target | Trigger | Required fields to write |
|---|---|---|---|
pending | running | Worker successfully claims the task | locked_by, locked_at, attempt |
running | success | Execution completes | finished_at, result_ref |
running | failed | Execution fails and cannot retry | finished_at, error_code, error_message |
running | retrying | Execution fails but can still retry | next_run_at, attempt, last_error |
retrying | pending | Next run time arrives | Clear or update claim fields |
running | timeout | Lease expires | finished_at, timeout_reason |
| Any terminal state | Any other state | Not allowed | Record and reject invalid transition |
This table is more valuable than a rushed block of generated code. Once the state machine is clear, APIs, databases, workers, and tests can grow around it. Without the table, AI may directly write task.status = "failed" or task.status = "success" in different functions, leaving the system to rely on string luck.
When implementing, do not scatter transition rules across business code. Centralizing them is much safer:
ALLOWED_TRANSITIONS = {
"pending": {"running"},
"running": {"success", "failed", "retrying", "timeout"},
"retrying": {"pending"},
"success": set(),
"failed": set(),
"timeout": set(),
}
def assert_transition(current: str, target: str) -> None:
if target not in ALLOWED_TRANSITIONS.get(current, set()):
raise ValueError(f"invalid transition: {current} -> {target}")
This code is not hard. AI can certainly write it. The real issue is that you must think to ask for it at the beginning, not after production has already produced a “successful task retried again” incident.
Find the Data Before Talking About Code
The beginning of a feature should not ask “how should the page be written” or “how should the API be written”. It should ask: what things in this feature will be stored, passed around, modified, aggregated, and discarded?
Take a comment system. The surface requirement is simple: users can post comments, others can reply, like, and delete.
If we jump straight into code, we can easily get several APIs:
POST /comments
POST /comments/{id}/replies
POST /comments/{id}/likes
DELETE /comments/{id}
But the skeleton of the system is in the data:
- Are comments a tree, or do we only allow one level of replies?
- Is deleting a comment a physical deletion, or should it display “this comment has been deleted”?
- Can a like be cancelled?
- Are hot comments ranked by like count, time, or a combined weight?
- Should replies under a deleted comment remain visible?
- Are comments visible before moderation?
- How many comments can one user post within a short period?
These are all data-structure and state-rule questions.
If you do not understand them, you will not know whether to ask AI for an adjacency list or path enumeration, whether deleted_at is needed, whether the like table needs a unique index, or whether hot ranking should be precomputed or calculated in real time.
AI can tell you several options, but it does not automatically know your business constraints. The developer’s value is here: translating vague requirements into explicit data structures.
For a comment tree, there are at least three common designs.
| Design | Core fields | Strengths | Weaknesses |
|---|---|---|---|
| Adjacency list | id, parent_id | Simple writes, good for shallow replies | Reading the whole tree needs recursion or multiple queries |
| Path enumeration | id, path, such as /1/8/23/ | Easy subtree query and intuitive ordering | Moving nodes is expensive, path must be maintained |
| Closure table | ancestor_id, descendant_id, depth | Fast ancestor and descendant queries | More complex writes, more rows |
If the product only allows one level of replies, an adjacency list is enough. If it allows deep threaded discussions and often expands whole trees, path enumeration is a better fit. If the scenario involves permission inheritance or organization structures where ancestors and descendants are queried frequently, a closure table may be more stable.
When asking AI to build a comment system, I would put these constraints directly into the prompt:
Comments support at most two levels: comments and replies. Infinite nesting is not supported.
Deleting a comment is a soft delete. Replies are kept, but the UI shows "original comment deleted".
The likes table needs a unique constraint on (comment_id, user_id).
Repeated likes return idempotent success.
Hot comments are sorted by score = like_count * 3 + reply_count - age_hours / 12.
The list API must be paginated and must not load all comments at once.
Behind this prompt is data-structure judgment. It defines tree depth, deletion semantics, uniqueness constraints, ranking logic, and pagination. Only then will AI-generated code move in the right direction.
Algorithms Are Not Showpieces; They Are the Order of Rules
Many algorithms in engineering code are not complex, but their order matters.
Consider a “upgrade user membership” workflow. It may need to:
- Validate the payment result
- Check whether the order has already been processed
- Update the membership expiration time
- Record benefit-change logs
- Issue coupons
- Send notifications
- Trigger analytics events
AI can write every individual step, but it cannot randomly order them.
If you send the notification before writing the database, a failure may make the user see incorrect information. If you issue coupons before confirming idempotency, duplicate callbacks may issue extra coupons. If analytics events are inside the transaction, a slow external system can block the main flow. If membership update and benefit logs are not in the same transaction, later audits may not reconcile.
In engineering, algorithms are often not advanced dynamic programming. They are the act of organizing a group of operations in the correct order and defining how failures settle.
If you write instructions to AI at this level, the result is much more stable:
Implement the membership upgrade workflow.
Constraints:
- Payment callbacks may repeat. Use the order number to guarantee idempotency.
- Membership expiration update and benefit-change log must be in the same database transaction.
- Coupon issuance, notification, and analytics events must not block the main transaction.
Write them to an outbox and let a background job handle them asynchronously.
- Every failure step must have an explicit state. Do not leave the order in an unexplained state.
The generated code changes noticeably because you are not asking it to improvise. You are giving it algorithmic order, transaction boundaries, and failure rules.
Here is a more typical algorithm example: dependency-based task scheduling.
Many background systems need “run A first, then B and C, and finally D”. It looks like a task queue, but it is really a directed acyclic graph. Topological sorting is more reliable than a pile of if/else logic.
If you do not mention topological sorting, AI may write a loop that repeatedly scans all tasks and executes tasks whose prerequisites have completed. With few tasks this works; with many tasks it becomes O(V^2) repeated scanning, and it is easy to miss cycle detection.
I would write the prompt like this:
Tasks have dependency relationships. Model them as a DAG.
Requirements:
- Use an indegree table and ready queue to perform topological sorting.
- If the final processed node count is smaller than the total node count, a cycle exists and the task group must fail.
- A task can enter ready only after all dependencies have succeeded.
- If any dependency fails, downstream tasks enter skipped and do not execute.
- Emit every task state transition for auditing.
The core pseudocode is simple:
from collections import defaultdict, deque
def topo_order(nodes, edges):
graph = defaultdict(list)
indegree = {node: 0 for node in nodes}
for before, after in edges:
graph[before].append(after)
indegree[after] += 1
ready = deque([node for node, degree in indegree.items() if degree == 0])
ordered = []
while ready:
node = ready.popleft()
ordered.append(node)
for next_node in graph[node]:
indegree[next_node] -= 1
if indegree[next_node] == 0:
ready.append(next_node)
if len(ordered) != len(nodes):
raise ValueError("dependency cycle detected")
return ordered
This algorithm is O(V + E), where V is the number of tasks and E is the number of dependency edges. Its value is not that “interviews may ask it”. Its value is that you can immediately see whether AI-written dependency scheduling repeatedly scans everything, lacks cycle detection, or wrongly puts downstream tasks into the ready queue after an upstream failure.
Complexity Still Needs to Be Counted
AI easily writes code that looks clear but is actually slow.
The most common cases are nested loops, per-row queries, loading without pagination, and scanning everything before filtering. With small data it runs. With real production data it collapses.
For example, to calculate each user’s order amount in the last 30 days, AI may write pseudocode like this:
users = db.query("select * from users")
for user in users:
orders = db.query("select * from orders where user_id = ?", user.id)
total = sum(o.amount for o in orders if o.created_at >= start)
result.append({"user_id": user.id, "total": total})
The logic is not wrong, but the complexity and access pattern are. It treats the database as a slow dictionary, performs N+1 queries, and moves filtering into the application layer.
This kind of statistic should usually push aggregation into the database:
select
user_id,
sum(amount) as total_amount
from orders
where created_at >= :start
group by user_id;
If only some users matter, constrain the user set:
select
o.user_id,
sum(o.amount) as total_amount
from orders o
join active_users u on u.id = o.user_id
where o.created_at >= :start
group by o.user_id;
There are three layers of complexity judgment behind this.
First, application-loop complexity. N users means N queries, so network round trips are O(N). If each query loads all orders for that user and filters in Python, larger total data makes it worse.
Second, database access paths. orders(created_at, user_id) and orders(user_id, created_at) are different indexes suited to different queries. If scanning global orders by time range, the former may be better. If querying recent orders for a small set of users, the latter may be better.
Third, whether the result should be precomputed. If the system checks rolling 30-day amounts every day, maintain a daily summary table and add 30 daily rows instead of scanning order details every time.
An experienced developer sees this requirement and first thinks about data scale:
- How many users are there?
- How many orders are there?
- Are we only counting active users?
- Is there an
orders(user_id, created_at)index? - Can aggregation SQL be used?
- Can results be precomputed by day?
This is not something to consider only in a later “performance optimization” phase. It belongs in the design phase. Once the data structure is wrong, asking AI to modify code later is mostly patching a wrong structure.
The same applies to Top K.
If the requirement is “find the 100 slowest entries from one million logs”, AI commonly sorts directly:
slowest = sorted(logs, key=lambda item: item.duration_ms, reverse=True)[:100]
That performs O(n log n) sorting. A better approach is to maintain a min-heap of size k:
import heapq
def top_k_slowest(logs, k=100):
heap = []
for item in logs:
entry = (item.duration_ms, item.request_id, item)
if len(heap) < k:
heapq.heappush(heap, entry)
elif entry[0] > heap[0][0]:
heapq.heapreplace(heap, entry)
return [entry[2] for entry in sorted(heap, reverse=True)]
Complexity changes from O(n log n) to O(n log k), and memory changes from O(n) to O(k). When k is 100, this is not a small optimization. It is the difference between a system that can work on streaming logs and one that cannot.
This is hard knowledge. You may not need to hand-write a heap every day, but you should know when a heap is appropriate and when full sorting is not acceptable.
Good Prompts Are Built on Professional Knowledge
Many people think a good prompt is mostly about language expression. Expression matters, but what really supports a good prompt is professional knowledge.
You know the difference between processes and threads, so you can tell AI which state must not live only in process memory. You know database transactions, so you can tell AI which operations must be atomic. You know queues and retries, so you can tell AI not to put external calls inside the main transaction. You know indexes, so you can notice when AI-generated queries perform full table scans.
AI covers a lot of knowledge, but it will not make responsibility judgments for you. It can offer “an implementation”. You must decide whether that implementation fits the current system.
So computer-science fundamentals have not depreciated in the AI era. Their usage has changed.
In the past, you learned data structures to write better code yourself. Now you learn data structures to know what data model AI should generate and how to fix it when it generates the wrong one.
In the past, you learned operating systems to understand processes, memory, files, and networking. Now you learn operating systems to understand why an AI-written background task blocks, why logs fill up a disk, and why a lock can drag down an entire service.
In the past, you learned databases to write SQL and design tables. Now you learn databases to know whether AI-written ORM code holds transactions too long or performs hidden N+1 queries on every request.
Do Not Rush Into Code
To avoid letting AI pile up code immediately, I suggest asking for four things in the first round.
First, the data model. Entities, fields, unique constraints, indexes, state enums, cache keys, and message structures. Without a data model, do not enter coding.
Second, the algorithm path. Explain what algorithm or mechanism the core flow uses: state machine, topological sorting, heap, hash deduplication, inverted index, batch processing, sliding window, or just a simple sequential flow. It does not need to be artificially complex, but it must be explicit.
Third, complexity and scale assumptions. For example: “a single user has at most 1,000 comments”, “five million orders per day”, “up to 100,000 task dependency edges”, or “interface P95 must be below 200 ms”. Different scale assumptions produce different solutions.
Fourth, acceptance examples. Include normal paths, boundary paths, failure paths, and concurrency paths. Without examples, generated code can only be reviewed by eye.
Here is a reusable prompt:
Do not write implementation code yet.
Based on the requirement below, output an engineering design draft:
1. Data model: entities, fields, unique constraints, indexes, state enums.
2. Core algorithm: explain the data structures and algorithms used, with complexity.
3. Scale assumptions: estimate data volume, concurrency, hotspots, and possible bottlenecks.
4. Failure paths: list exceptions, retry, idempotency, timeout, cancellation, and rollback.
5. Acceptance examples: at least 8 test cases, including boundary and concurrency cases.
After outputting the draft, wait for my confirmation. Do not write code directly.
The value of this prompt is not its format. Its value is that it surfaces the design first. Once the design is visible, a programmer can review it. If AI directly generates 1,000 lines of code, many mistakes hide deep inside.
A More Reliable Working Order
When using AI to write features, I increasingly prefer this order:
First ask AI to restate the problem without writing code. Focus on whether it understands the user, data, state, and boundaries.
Then ask AI to output data structures. This includes database tables, JSON schemas, state enums, indexes, cache keys, and message bodies. If this part is unclear, the later code will be messy.
Next ask AI to output algorithmic flow. Do not write only the happy path. Include failure paths, retry paths, concurrency paths, and cancellation paths.
Then ask AI to write test examples. Not vague “test success and failure”, but concrete inputs, expected states, database changes, and external side effects.
Only then ask AI to write code.
This order looks slower, but it is actually faster. AI writes code too quickly. The real waste of time is discovering after the code is written that the structure was wrong.
Back to This Article
This article did not stop at the old phrase “programs = algorithms + data structures”. It broke the phrase into a more concrete checklist: arrays, hash tables, queues, heaps, trees, graphs, inverted indexes, Bloom filters, LRU, and state machines, and what kinds of real-system problems they become.
It also gave several practical examples: task state machines, comment-tree modeling, topological sorting for dependent tasks, aggregation SQL for order statistics, and heap-based Top K. These examples are not meant to show off algorithms. They are meant to explain one thing: AI can write code, but the programmer must know what structure, algorithm, and complexity to ask it for.
This article has not yet expanded on process communication, module interaction, database transactions, or high concurrency. Those will appear in later articles. The job of the first article is only to lay the foundation: programmers in the AI era cannot merely accept UI output; they must be able to accept program structure.
If that foundation is unstable, every attempt to “let AI build a system” will become code piled on sand.
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.