TechluminateSystems consulting
← Field Notes

Software Architecture

The First Bug Appears Before You Write Code

The first bug in a product often appears in an assumption—not in code. A reading-game idea shows how abstraction begins between an idea and reality.

Jerry Bony

12 min read

The first bug in my reading application did not appear in the code. It appeared in an assumption I had never stopped to question.

I had an idea: build an online game that teaches children how to read and write.

Almost immediately, my mind began filling in the details.

I imagined an animated world, a main character, reading challenges, voice recognition, rewards, avatars, parent dashboards, competitions, and progress reports. I could already picture children exploring the world and unlocking new places by learning words.

It felt like I was designing a product.

But I had not answered the most important question:

What exactly should a child be able to do after using it?

I was already thinking about the machinery before I had defined the result.

That realization changed how I understood abstraction.

My incomplete definition of abstraction

I used to think abstraction was mainly about hiding complicated code behind a simple function.

For example:

ts
evaluateReadingAttempt(recording)

Someone calling this function should not need to understand everything happening inside it. They should not need to know how the audio is cleaned, converted into text, compared with a passage, scored, and prepared for adult review.

The function exposes the goal and hides the machinery.

That definition was not wrong, but it was incomplete.

Abstraction begins before the function exists. It begins before the database, interface, framework, or programming language.

It begins in the space between an idea and reality.

Everything begins in theory

Before we create something in physical reality, we normally create a model of it in our minds.

A person building a house first imagines what the house should provide. Someone preparing a meal imagines the finished dish. Someone planning a trip begins with a destination.

Software works the same way.

We begin with something theoretical:

  • A person we want to help
  • A problem we want to solve
  • A result we want to produce
  • A behavior we believe will produce it

We then move toward the physical:

  • Screens
  • Buttons
  • Components
  • Servers
  • Databases
  • Tests
  • Deployed software

Theory represents what could become real. Implementation gives that possibility a concrete form.

If the theory is unclear, the physical result may be technically impressive while solving the wrong problem.

Returning to the child

I returned to the original idea:

Build a game that helps children learn to read and write.

That statement was too broad. It did not tell me which children I wanted to help or what successful learning meant.

So I started at the end.

I imagined a learner completing the program and asked what that child should be able to demonstrate.

The provisional outcome became:

A child between the ages of 8 and 12 should be able to read a 200-word story aloud, correctly answer at least four of five comprehension questions, and write a summary of the story.

This goal would still need to be validated by a literacy educator and tested with real learners. But it gave the product direction.

I could now think backward.

If the child needed to read and summarize a story, the child would first need to:

  1. Recognize letters and sounds
  2. Blend sounds into words
  3. Read phrases and sentences
  4. Understand short paragraphs
  5. Answer questions about a passage
  6. Express that understanding in writing

None of this required me to choose a framework.

I was defining what needed to become true before deciding how software would make it happen.

Moving through the abstraction layers

At the highest level, I had the purpose:

Help struggling children become confident readers and writers.

Below that was the observable outcome:

Read a 200-word story, answer comprehension questions, and write a summary.

Below that was the learning process:

text
Assess → Teach → Practice → Give feedback → Retry → Verify learning

Below that were the important concepts:

text
Learner Concept Lesson Session Challenge Attempt Feedback Mastery

Only after clarifying those layers would I move into the physical implementation:

text
Interface Audio recording API requests Database records Speech analysis Progress calculations Deployment

This is how an idea moves from theory into concrete reality.

Each layer makes the idea more specific. Each lower layer must serve the purpose established above it.

Discovering that words matter

The abstraction process forced me to examine words I had initially taken for granted.

Consider this rule:

After three incorrect attempts, teach the concept again and provide an easier activity.

It sounds clear until I try to implement it.

What is an attempt?

Does it begin when the child sees a question or when the child submits an answer? Does it end when feedback is provided? If the child tries again, is that part of the same attempt or a new one?

What if the child uses a hint? What if the microphone fails? What if the three mistakes occur across different sessions? What if they belong to different concepts?

The database cannot answer these questions for me.

I must first define the theoretical meaning.

I arrived at these working definitions:

  • A concept is a specific literacy skill being taught.
  • A lesson provides instruction and practice for one or more concepts.
  • A session is one bounded period of learning activity.
  • A challenge asks the learner to demonstrate a skill.
  • An attempt begins when a learner responds to a challenge and ends when the response is evaluated and feedback is provided.

A retry is therefore a new attempt on the same challenge.

text
Challenge ├── Attempt 1 → Incorrect ├── Attempt 2 → Incorrect └── Attempt 3 → Correct

These definitions are abstractions. They give us a simple language for reasoning about a complicated learning process.

They also make the eventual code easier to discover and understand. A developer who needs to modify retry behavior knows to look for Attempt, Challenge, or PracticeSession.

The language of the product begins to appear inside the software.

Turning theory into a concrete rule

Once the concepts were clearer, I could describe the repeated-mistake behavior:

text
Given a learner is practicing a concept And has already made two incorrect attempts When the learner makes a third incorrect attempt Then the concept is explained again in a different way And the learner receives an easier problem targeting the same skill

This scenario sits between the idea and the implementation.

It is more concrete than saying, “Help the child when they struggle.” But it is still independent of databases, interfaces, and frameworks.

It exposes what matters:

  • The learner is practicing a particular concept.
  • The learner has experienced repeated difficulty.
  • The system must respond educationally.
  • The explanation should change.
  • The next problem should be easier.
  • The easier problem must still reinforce the same skill.

It hides what we do not need to know at this level:

  • Where attempts are stored
  • How the attempt count is calculated
  • How the alternate explanation is selected
  • Which component renders the activity
  • Which database is used
  • Which programming language implements it

This is the bridge between theory and reality.

The behavior can now become a test. The test can guide the code. The code can make the behavior real.

Essential complexity

Some of the complexity in this feature belongs to the actual teaching problem.

A human tutor would still need to determine:

  • Whether the learner’s answer is incorrect
  • Whether repeated errors indicate a misunderstanding
  • How to explain the concept differently
  • Which easier activity teaches the same skill
  • When the learner is ready to return to the original difficulty

These decisions exist even without an application.

That is essential complexity.

Abstraction does not eliminate it. It gives it names, boundaries, and a structure we can reason about.

Accidental complexity

Now imagine that I ignored those questions and began with the physical implementation.

I created:

  • An attempts table
  • An answer-submission API
  • Retry counters
  • Speech-recognition integration
  • Progress calculations
  • Interface state
  • Network-error handling

Then I discovered that only consecutive, unassisted attempts on the same concept should count. A microphone failure should not count as a learner mistake. A tutor should be able to correct an automatic evaluation. The count might reset in a new session.

My database and APIs may no longer represent the real rules. Parts of the system may need to be redesigned.

That rework is accidental complexity.

I created difficulty by implementing a solution before understanding the problem.

The technology was not necessarily bad. It was simply given authority too early.

Instead of letting the learning rules shape the software, I allowed the software structure to shape the learning rules.

Reality provides feedback

Theory is not perfect just because it comes first.

The 200-word goal may be inappropriate. Three attempts may not be the correct threshold. The proposed learning sequence may contain mistakes. A child may respond differently from what I imagined.

That is why the movement from theory to reality is not a one-way trip.

We form a theory. We create a concrete version. Reality gives us feedback. We then refine the theory and create a better version.

text
Theory ↓ Concrete implementation ↓ Real-world feedback ↓ Improved theory ↓ Improved implementation

The purpose of thinking first is not to predict everything perfectly. It is to make our guesses visible enough to test.

The first reading-game version does not need an enormous world, automatic speech analysis, multiplayer competition, payments, and twelve weeks of finished content.

It needs one complete learning experience:

  1. Determine the learner’s starting point.
  2. Teach one concept.
  3. Provide one practice challenge.
  4. Respond appropriately to mistakes.
  5. Verify whether the learner improved.
  6. Show the result to a parent or tutor.

That small piece of reality can teach me more than months spent designing a system no child has used.

How this makes me a better developer

This changed how I see software development.

A developer’s value does not come only from knowing syntax, frameworks, and tools. Those skills matter, but they belong to the physical layer.

A valuable developer can move between theory and reality.

They can listen to an unclear idea and ask:

  • Who are we helping?
  • What are they trying to achieve?
  • What would prove success?
  • Which rules belong to the real problem?
  • Which complexity comes from our implementation choices?
  • What is the smallest complete behavior we can make real?

They can then translate the answers into meaningful concepts, tests, interfaces, and working software.

This ability improves the developer use cases that matter every day:

  • Discovering where behavior lives
  • Understanding how a feature currently works
  • Adding new behavior
  • Changing existing rules
  • Debugging failures
  • Testing outcomes
  • Planning future work

Good abstraction makes the code communicate the system’s purpose.

How this connects to making money

Customers do not pay for abstractions because abstractions sound intelligent. They pay for useful outcomes.

A parent would not buy the reading application because it has React components, API routes, or a sophisticated database.

They would consider paying because they believe it can help their child become a more capable and confident reader.

The code enables the value. It is not the value itself.

This distinction matters whether I am an employee, freelancer, consultant, or founder.

As an employee, abstraction helps me clarify requirements, identify missing rules, reduce defects, and build features that are easier to change.

As a freelancer, it helps me understand what a client actually needs instead of blindly implementing the solution they initially requested.

As a product creator, it helps me test the central value before spending months on supporting features.

It would be easy to spend a great deal of time and money building animated worlds, dashboards, leaderboards, speech recognition, and social competition.

But if the basic learning loop does not help a child improve, those features have little economic value.

Abstraction helps protect time, money, and attention by directing them toward the most important uncertainty first.

It does not guarantee that a product will make money. It helps prevent me from wasting money while building something whose value I have never demonstrated.

From potential to reality

Every software product begins as potential.

At first, it could become almost anything. That freedom can feel exciting, but it also contains enormous complexity.

Abstraction helps constrain that potential.

It asks me to decide:

  • Which outcome matters
  • Which behavior is essential
  • Which concepts need names
  • Which details can remain hidden
  • Which guesses require testing
  • Which implementation is sufficient for now

Then I can move downward—from the theoretical to the physical, from the general to the specific, and from the idea to working software.

I no longer see abstraction as simply wrapping complicated code inside a function.

I see it as the process of carrying an essential truth through every layer of development until it takes a concrete form in reality.

The goal shapes the behavior.

The behavior shapes the model.

The model shapes the tests.

The tests guide the code.

The code produces an experience.

The experience creates—or fails to create—real value.

That is how we work.

We begin with an idea about what could be true. Then we use design, code, testing, and feedback to make it real.