Mr. Editor-in-chief Mr. Editor-in-chief 17 min read 3347 words 11 views

Software Fundamentals Matter More Than Ever in the Age of AI Coding

Introduction

AI coding tools have changed how software gets written. They can generate features, modify existing code, write tests, refactor modules, and explore a codebase at impressive speed.

But speed creates a new problem: bad code can now be produced faster than ever.

That is why software fundamentals matter more, not less. The developer’s job is no longer only to type every line of code. The developer’s job is to guide the system, shape the architecture, define the boundaries, preserve quality, and make sure the codebase stays easy to change.

This tutorial explains how to work with AI coding tools without letting your codebase collapse into unmaintainable complexity.


1. The Problem with “Specs to Code”

A common AI coding workflow looks like this:

  1. Write a specification.
  2. Ask AI to generate code.
  3. If the app is wrong, update the specification.
  4. Generate more code.
  5. Repeat.

This sounds attractive because it suggests you no longer need to care about the code itself. The specification becomes the source of truth, and the generated code becomes disposable.

In practice, this often fails.

Each generation may make the codebase slightly worse. The first result may be acceptable. The second result may be messier. After several rounds, the code can become difficult to understand, test, or modify.

The problem is simple:

text
Ignoring the code does not make code quality irrelevant.
It makes code quality invisible until it becomes expensive.

AI does not remove the need for software design. It makes software design more important because AI can create complexity very quickly.


2. Bad Code Is More Expensive Than Ever

There is a popular idea that “code is cheap” because AI can generate it quickly.

That is only half true.

Generating code is cheap. Maintaining bad code is expensive.

A bad codebase is one that is hard to change. If every modification creates bugs, breaks unrelated features, or requires digging through unclear dependencies, the codebase is expensive no matter how quickly it was produced.

A good codebase is easy to change.

That means:

  • The design is understandable.
  • The boundaries are clear.
  • The modules are testable.
  • The naming is consistent.
  • The system can absorb new requirements without collapsing.

AI works much better in a good codebase. When the structure is clear, the model can navigate it, understand it, and make safer changes. When the structure is chaotic, the model is more likely to misunderstand the system and produce even more chaos.


3. Failure Mode 1: The AI Does Not Build What You Wanted

One of the most common AI coding failures is this:

text
You had a clear idea in your head.
The AI built something else.

This usually happens because you and the AI do not share the same design concept.

The “design concept” is the invisible shared understanding of what is being built. It includes the product idea, user expectations, constraints, trade-offs, edge cases, and the shape of the solution.

A short prompt rarely transfers that understanding.

Bad prompt

text
Build a dashboard for managing customers.

The AI has too many unanswered questions:

  • What is a customer?
  • What fields matter?
  • Who uses the dashboard?
  • What actions are allowed?
  • What should happen when data is missing?
  • Are there roles and permissions?
  • Should it be mobile-friendly?
  • Is this internal or customer-facing?

Better workflow: let the AI interview you

Before asking for implementation, ask the AI to question you.

text
Interview me relentlessly about this feature until we reach a shared understanding.

Walk through the product requirements, user flows, edge cases, data model, permissions, UI behavior, and technical constraints.

Do not write code yet.

Ask one group of questions at a time. After I answer, continue until the plan is clear enough to turn into an implementation document.

This changes the AI from a code generator into a requirements-gathering partner.

The result can later become:

  • A product requirements document
  • A technical plan
  • A task list
  • GitHub issues
  • Implementation steps

Best practice

Do not rush into code. First create shared understanding.

Use this pattern:

text
1. Discuss the feature.
2. Let the AI ask questions.
3. Resolve unclear decisions.
4. Write a short plan.
5. Review the plan.
6. Only then implement.

4. Failure Mode 2: The AI Is Too Verbose and Confusing

Sometimes the AI produces long explanations, unclear plans, or code that uses inconsistent terminology.

This is often a language problem.

Human teams solve this with shared vocabulary. In domain-driven design, this is called a ubiquitous language: a common set of terms used by developers, domain experts, documentation, and code.

The same idea helps with AI.

If your application uses five different words for the same concept, the AI may become confused.

For example:

text
client
customer
account
buyer
organization

Are these the same thing? Different things? Does one contain another? Can a customer belong to multiple organizations?

If the project does not define these terms clearly, the AI may make incorrect assumptions.

Create a shared language document

Add a file like this:

markdown
# Ubiquitous Language

| Term | Meaning | Notes |
|---|---|---|
| Customer | A person or company that buys from us | Used in billing and orders |
| Account | Login identity for a user | Not the same as Customer |
| Organization | A company containing multiple accounts | Used for team access |
| Order | A purchase request from a customer | Has payment and fulfillment status |
| Invoice | A billing document generated from an order | May be paid or unpaid |

Then tell the AI:

text
Before planning or coding, read `docs/ubiquitous-language.md`.

Use these terms consistently in code, comments, filenames, tests, and documentation.

If a new term is needed, ask me before inventing one.

Why this helps

A shared language improves:

  • Planning
  • Naming
  • Code organization
  • Documentation
  • Test clarity
  • AI reasoning
  • Human review

It also reduces verbosity because the AI can use precise project-specific terms instead of explaining around unclear concepts.


5. Failure Mode 3: The AI Builds the Right Thing, But It Does Not Work

Another common failure:

text
The AI understood the feature.
The implementation looks plausible.
But it does not actually work.

This is where feedback loops matter.

AI needs fast, reliable signals that tell it whether its code is correct.

Good feedback loops include:

  • Static types
  • Type checking
  • Automated tests
  • Linters
  • Browser inspection
  • Runtime errors
  • Build output
  • Integration tests

For TypeScript projects, a type checker is one of the most valuable feedback loops.

bash
npm run typecheck

For tests:

bash
npm test

For linting:

bash
npm run lint

For building:

bash
npm run build

The exact commands depend on your project, but the principle is the same: AI should not generate a large pile of code and only then check whether it works.


6. The Rate of Feedback Is the Speed Limit

AI often tries to do too much at once.

It may modify many files, add several abstractions, create tests, change types, and refactor existing code in a single pass. This feels productive, but it is risky.

A better rule is:

text
The rate of feedback is the speed limit.

If feedback is slow or unreliable, take smaller steps.

Bad AI workflow

text
1. Generate a large feature.
2. Modify many files.
3. Run tests at the end.
4. Discover many failures.
5. Ask AI to fix everything.
6. Code gets messier.

Better AI workflow

text
1. Write or update one test.
2. Make the smallest implementation change.
3. Run the test.
4. Run the type checker.
5. Refactor.
6. Repeat.

This is why test-driven development works well with AI.


7. Use TDD to Control AI Coding

Test-driven development gives AI a disciplined loop:

text
Red → Green → Refactor

Meaning:

  1. Write a failing test.
  2. Write code to make the test pass.
  3. Refactor while keeping the test passing.

Prompt:

text
Use test-driven development for this change.

Rules:
1. Do not implement the feature first.
2. First write a failing test that describes the desired behavior.
3. Run the test and confirm it fails for the expected reason.
4. Implement the smallest change needed to pass.
5. Run the test again.
6. Refactor only after the test passes.
7. Keep each step small.

This prevents the AI from outrunning its feedback loops.


8. Testing Is Hard Because Design Is Hard

Testing is not just about writing test files. Good testing requires design decisions:

  • What unit should be tested?
  • What behavior matters?
  • What should be mocked?
  • What should be real?
  • How large should the test boundary be?
  • How flaky is the test likely to become?
  • How much setup is required?

If the codebase is poorly designed, tests become painful.

A hard-to-test codebase usually has:

  • Too many tiny modules
  • Unclear boundaries
  • Hidden dependencies
  • Complex interfaces
  • Tight coupling
  • Side effects spread everywhere

A testable codebase usually has:

  • Clear module boundaries
  • Simple interfaces
  • Predictable inputs and outputs
  • Limited side effects
  • Good separation of concerns

This leads to one of the most important architectural ideas for AI-assisted development: deep modules.


9. Design Deep Modules, Not Shallow Modules

A shallow module has little functionality but a complicated interface.

A deep module has a lot of functionality hidden behind a simple interface.

Shallow module

text
- Many small files
- Many tiny functions
- Many dependencies
- Lots of coordination required
- Hard to know where behavior lives

Deep module

text
- Clear boundary
- Simple interface
- Internal complexity hidden
- Easy to test from the outside
- Easier for humans and AI to understand

A deep module does not mean a giant messy file. It means a well-designed boundary that hides internal complexity.

Example:

ts
// Good external interface

const invoice = await billing.createInvoice({
  customerId,
  orderId,
  currency: "HKD",
});

The caller does not need to know all the internal steps:

  • Tax calculation
  • Discount rules
  • Invoice numbering
  • Database writes
  • Payment status initialization
  • Audit logging

Those details can live inside the billing module.


10. Refactor Toward Better Architecture

If your codebase is already messy, do not ask AI to rewrite everything at once.

Ask it to identify opportunities for deeper modules.

Prompt:

text
Analyze this codebase and look for opportunities to improve the architecture.

Focus on:
- Shallow modules
- Repeated logic
- Unclear boundaries
- Files that know too much
- Code that is hard to test
- Related behavior spread across many places

Do not modify code yet.

First produce:
1. A short architecture diagnosis
2. Candidate module boundaries
3. Suggested deep modules
4. Risks of each refactor
5. A step-by-step migration plan

Then refactor one boundary at a time.

text
Now implement only step 1 of the migration plan.

Rules:
- Keep behavior unchanged.
- Add or update tests first.
- Move code behind the new interface.
- Run typecheck and tests after the change.
- Do not continue to step 2 until I approve.

This keeps AI work controlled and reviewable.


11. Design the Interface, Delegate the Implementation

AI is often good at tactical implementation. It can fill in internal logic, write repetitive code, update call sites, generate tests, and handle boilerplate.

But the human developer should stay responsible for strategic design.

A useful division of labor is:

text
Human: design the module boundary.
AI: implement inside the boundary.
Human: review the interface and tests.
AI: fix implementation details.

For less critical parts of an application, you may not need to inspect every internal line deeply if the module has:

  • A clear purpose
  • A simple interface
  • Strong tests
  • Good type coverage
  • Limited side effects

You can treat the module as a gray box: not completely ignored, but not mentally loaded in full all the time.

Example prompt

text
I want to create a deep module for billing.

First help me design the public interface.

Do not implement yet.

The module should handle:
- Creating invoices
- Applying discounts
- Calculating tax
- Marking invoices as paid
- Querying invoice status

Please propose:
1. The public functions
2. Input and output types
3. Error cases
4. Test scenarios
5. What should remain internal

After reviewing the interface:

text
Now implement the billing module behind this interface.

Rules:
- Do not change the public interface without asking.
- Add tests at the interface boundary.
- Keep internal helpers private.
- Run typecheck and tests.

12. Keep Architecture in Your Planning Documents

When planning a change, do not only describe product behavior. Also describe module impact.

A weak implementation plan says:

text
Add coupons to checkout.

A better plan says:

markdown
# Feature: Coupons in Checkout

## User Behavior

Users can enter a coupon code during checkout and see the discount before payment.

## Module Changes

### `checkout`
- Calls coupon validation before final price calculation.
- Displays discount result to the user.

### `coupons`
- New deep module.
- Owns coupon validation rules.
- Owns coupon usage limits.
- Exposes a simple validation interface.

### `billing`
- Receives final discounted amount.
- Does not know coupon business rules.

## Public Interface

```ts
validateCoupon({
  code: string;
  customerId: string;
  cartTotal: number;
}): Promise<CouponValidationResult>

Tests

  • Valid coupon applies discount.
  • Expired coupon is rejected.
  • Coupon below minimum cart value is rejected.
  • Coupon cannot be reused if usage limit is reached.

Planning at this level keeps the system design alive.

---

## 13. Invest in the Design Every Day

AI makes it tempting to focus only on immediate output:

```text
Add this.
Fix that.
Generate this.
Patch that.

But if every change only solves the local problem, the global design slowly decays.

This is software entropy: every careless change makes the next change harder.

To fight entropy, invest in the design continuously.

Daily habits:

  • Rename unclear concepts.
  • Strengthen module boundaries.
  • Remove duplicated logic.
  • Simplify interfaces.
  • Add tests around important behavior.
  • Update the shared language document.
  • Refactor after tests pass.
  • Keep implementation details hidden.
  • Review AI-generated changes for architecture, not only correctness.

AI can help with this, but it should not own the architecture alone.


14. Practical AI Coding Workflow

Here is a full workflow you can use for AI-assisted development.

Step 1: Establish shared understanding

text
Interview me about this feature until the requirements are clear.

Do not write code yet.

Ask about:
- User goals
- Edge cases
- Data model
- Permissions
- UI behavior
- Failure states
- Existing modules affected
- Testing strategy

Step 2: Define terminology

text
Read `docs/ubiquitous-language.md`.

If this feature introduces new domain terms, propose additions to the document before coding.

Step 3: Plan module changes

text
Create an implementation plan.

Include:
1. Product behavior
2. Module boundaries
3. Public interfaces
4. Internal implementation notes
5. Tests to write
6. Migration risks

Do not modify code yet.

Step 4: Use TDD

text
Use TDD.

First write a failing test at the correct module boundary.
Then implement the smallest change needed to pass.
Then refactor.

Step 5: Run feedback loops frequently

bash
npm run typecheck
npm test
npm run lint
npm run build

Step 6: Review architecture

text
Review the final change.

Check:
- Did we preserve module boundaries?
- Did we introduce shallow modules?
- Is the interface simple?
- Are tests at the right level?
- Did we update terminology or docs?
- Is the codebase easier or harder to change now?

15. Troubleshooting Common AI Coding Problems

Problem: The AI built the wrong thing

Cause: unclear requirements or missing shared design concept.

Fix:

text
Stop implementation. Interview me until the requirements are clear. Then rewrite the plan before coding.

Problem: The AI uses too many vague words

Cause: no shared project vocabulary.

Fix:

text
Create a ubiquitous language document for this codebase. Identify key domain terms and use them consistently.

Problem: The AI wrote a lot of code that does not work

Cause: weak feedback loops or too-large implementation steps.

Fix:

text
Revert to the last working state. Re-implement using TDD in small steps.

Problem: Tests are hard to write

Cause: poor module boundaries.

Fix:

text
Analyze this code and propose deeper module boundaries that would make testing easier.

Problem: The AI keeps touching too many files

Cause: unclear architecture and uncontrolled scope.

Fix:

text
Limit the change to one module. Ask before modifying files outside that module.

Problem: Reviewing AI code is exhausting

Cause: too much implementation detail lacks boundaries.

Fix:

text
Design clear interfaces and test at those interfaces. Delegate internal implementation only when the boundary is safe.

16. Best Practices for AI-Assisted Software Development

Do

  • Treat AI as a tactical coding partner.
  • Keep yourself responsible for strategy and design.
  • Ask AI to clarify requirements before coding.
  • Maintain a shared language document.
  • Use tests, types, and builds as feedback loops.
  • Make small changes.
  • Prefer deep modules with simple interfaces.
  • Review public interfaces carefully.
  • Refactor regularly.
  • Keep architecture visible in planning documents.

Do Not

  • Treat generated code as disposable garbage.
  • Keep regenerating code from specs without reviewing design.
  • Let AI make large changes without tests.
  • Allow terminology to drift.
  • Accept shallow module sprawl.
  • Delay feedback until the end.
  • Confuse fast output with maintainable progress.

17. Useful Prompt Templates

Requirements Interview Prompt

text
Interview me relentlessly about this feature until we reach a shared understanding.

Walk through:
- User goals
- User flows
- Edge cases
- Data model
- Permissions
- UI states
- Error handling
- Existing modules affected
- Testing strategy

Do not write code yet.
Only ask questions and summarize decisions.

Ubiquitous Language Prompt

text
Scan this codebase and identify important domain terminology.

Create or update `docs/ubiquitous-language.md`.

Include:
- Term
- Meaning
- Where it appears
- Related terms
- Terms that may be confusing or duplicated

Do not change implementation code yet.

TDD Prompt

text
Use test-driven development for this change.

Rules:
1. Write a failing test first.
2. Confirm it fails for the expected reason.
3. Implement the smallest passing change.
4. Run tests and typecheck.
5. Refactor only after passing.
6. Keep changes small.

Architecture Improvement Prompt

text
Analyze this codebase for shallow modules and unclear boundaries.

Do not modify code yet.

Return:
1. Current architecture problems
2. Candidate deep modules
3. Suggested public interfaces
4. Testing strategy
5. Step-by-step refactor plan
6. Risks and rollback plan

Interface-First Prompt

text
Help me design the public interface for this module before implementation.

Focus on:
- Inputs
- Outputs
- Error cases
- Naming
- Testability
- What should remain private

Do not implement until I approve the interface.

Final Summary

AI coding tools make software development faster, but they do not make software fundamentals obsolete.

In fact, they raise the value of fundamentals.

When AI can generate code quickly, the bottleneck shifts to design quality, feedback loops, architecture, naming, testing, and maintainability.

The best developers in the AI era are not the ones who ignore the code. They are the ones who guide the system strategically.

AI can be the fast tactical programmer. You still need to be the software designer.

Copied to clipboard

Share this post

Related Posts