DEV Community

Derek Mwale
Derek Mwale

Posted on

Learning to See the Shape of a Problem

There is a moment in software development when a problem stops looking like a collection of bugs.

You stop staring at the error message.

You stop staring at the database table.

You stop staring at the function that failed.

And suddenly, you begin to see something else.

A shape.

The problem has a structure.

It has boundaries.
It has movement.
It has pressure points.
It has inputs and outputs.
It has things that depend on other things.
It has things that happen repeatedly.
It has things that happen only under certain conditions.

This is one of the most valuable skills a developer can develop.

Not simply learning how to solve problems.

Learning how to see them.

Because before you can design a solution, you need to understand what kind of problem you are actually looking at.

Sometimes a problem is a performance problem.

Sometimes it is a data problem.

Sometimes it is a state problem.

Sometimes it is a communication problem between components.

Sometimes it is a workflow problem disguised as a technical bug.

And sometimes, what looks like five different problems is actually one problem appearing in five different places.

The ability to recognize that difference changes how you build software.


The Problem Behind the Problem

Imagine an application where users occasionally report that a page takes too long to load.

The first instinct might be to inspect the frontend.

Perhaps the JavaScript is slow.

Perhaps the component is rendering too much.

Perhaps the browser is doing unnecessary work.

But suppose the frontend is perfectly fine.

The API is taking 4 seconds.

So now we inspect the API.

Perhaps the API performs several database queries.

We optimize one query.

The response improves.

But another endpoint becomes slow.

Then we discover that several endpoints are requesting the same information independently.

Now the problem looks different.

It is no longer simply:

"This page is slow."

It becomes:

"The system repeatedly performs overlapping work to construct related information."

That is a much more useful description.

The original symptom was a page.

The actual shape was a work duplication pattern.

This distinction matters.

Symptoms are often local.

Problems are often structural.


Every Problem Has a Shape

When I think about software systems, I increasingly think about problems geometrically.

Not because every problem can literally be represented as a geometric object, but because relationships are easier to understand when we stop looking at individual pieces.

Consider a simple application.

A user creates an order.

The order contains products.

Products belong to categories.

The payment system processes the order.

The inventory system updates stock.

The notification service informs the user.

The analytics system records the transaction.

A single action creates a chain.

User
|
v
Order
|
+----> Payment
|
+----> Inventory
|
+----> Notification
|
+----> Analytics

The system is not simply a collection of functions.

It is a network of relationships.

The shape of the network tells us something about the system.

If everything depends on one central component, we have one shape.

If components communicate independently, we have another.

If requests travel through several layers, we have another.

If information constantly moves between services, we have another.

Architecture becomes easier to understand when we begin seeing these shapes.

And debugging becomes easier too.


Start With Movement

One of the simplest ways to understand a problem is to ask:

What is moving?

Data is constantly moving through software.

A user enters information.

The browser sends it to an API.

The API validates it.

The application transforms it.

The database stores it.

Another service retrieves it.

A notification system consumes it.

A report eventually summarizes it.

The data has traveled.

When something goes wrong, tracing that movement can reveal the shape of the problem.

For example:

Input
↓
Validation
↓
Transformation
↓
Storage
↓
Processing
↓
Output

Now imagine that incorrect information appears at the output.

Where did it become incorrect?

That question is much more useful than immediately asking which function is broken.

You move backward through the system.

Was the input incorrect?

Was validation incomplete?

Did transformation modify the value?

Did storage change the representation?

Did processing use an incorrect assumption?

The problem begins to reveal its location.

This is why data-flow thinking is so powerful.

You are not merely looking for broken code.

You are following the journey of information.


Look for Boundaries

Every system has boundaries.

A frontend has a boundary with an API.

An API has a boundary with a database.

A payment system has a boundary with a payment provider.

A microservice has a boundary with another service.

Even functions have boundaries.

These boundaries are important because problems often appear where things meet.

One system expects:

{
"user_id": 42
}

Another system sends:

{
"id": 42
}

Both systems might work perfectly on their own.

The problem exists between them.

That is a different shape.

It is an interface problem.

This is why developers should not only inspect components individually.

Sometimes you need to inspect the space between them.

The connections matter just as much as the nodes.


The Shape of Repetition

Repetition is another powerful clue.

Suppose a developer notices that the same bug appears every Monday.

That is interesting.

The problem may not be random.

Maybe a scheduled process runs every Monday.

Maybe a weekly data import creates unusual records.

Maybe traffic patterns change.

Maybe a recurring job interacts with another system.

The timing itself becomes information.

The same idea applies inside code.

Suppose five different modules independently calculate the same value.

That repetition might indicate that a shared concept exists but has not been modeled explicitly.

You might have:

Module A → calculate total
Module B → calculate total
Module C → calculate total
Module D → calculate total
Module E → calculate total

The repeated behavior is a clue.

Perhaps the system wants:

Total Calculator
/ | \
A B C

The repetition reveals a missing abstraction.

This is one of the interesting parts of software engineering.

Sometimes duplication is not merely duplication.

It is evidence.


The Shape of State

Some of the hardest problems in software are state problems.

The application behaves correctly when everything is simple.

Then users perform actions in a different order.

And suddenly the system behaves strangely.

Consider an order.

Created
↓
Paid
↓
Shipped
↓
Delivered

Now imagine someone tries to cancel the order after it has been shipped.

The question is no longer simply:

"Does the cancel function work?"

The deeper question is:

"Is cancellation a valid transition from this state?"

Now we are looking at a state machine.

Created
├──> Cancelled
└──> Paid
└──> Shipped
└──> Delivered

The shape explains the problem.

This way of thinking can be applied to payments, subscriptions, authentication, workflows, logistics, inventory, games, booking systems, and countless other applications.

Whenever behavior depends on what happened previously, you probably have a state problem.


The Shape of Time

Software problems also have temporal shapes.

Some happen once.

Some happen gradually.

Some happen after repeated operations.

Some appear only after the system has been running for several hours.

Imagine memory usage:

Memory
|
| /
| /
| /
| /
|/_________ Time

That shape tells a story.

Memory is increasing over time.

That suggests a different investigation than a sudden memory spike:

Memory
|
| /\
| / \
|_/ ____ Time

The second shape might represent a temporary workload.

The first might suggest resources are not being released as expected.

The graph becomes a form of debugging.

This is why observability is so valuable.

Logs, metrics, traces, and events transform invisible behavior into visible shapes.


Problems Have Dimensions

Sometimes developers investigate only one dimension.

They ask:

What is the code doing?

But there are other questions.

When is it happening?

How often is it happening?

Who is affected?

What data is involved?

What happens immediately before it?

What happens afterward?

What does the system depend on?

What changes when the workload increases?

These dimensions turn a vague problem into something measurable.

Imagine an API that works perfectly for ten users but struggles with ten thousand.

The functionality did not necessarily change.

The scale changed.

Now the shape of the problem is different.

Perhaps:

Workload ↑
|
v
Requests ↑
|
v
Database queries ↑
|
v
Latency ↑

This is a scaling shape.

You are no longer debugging a single request.

You are examining how system behavior changes as conditions change.


From Symptoms to Patterns

One of the biggest changes in engineering maturity is learning to move from symptoms to patterns.

A symptom might be:

"The checkout button sometimes fails."

A pattern might be:

"Checkout failures increase when inventory synchronization is running."

That second observation is far more valuable.

Another symptom:

"Some reports contain duplicate records."

Pattern:

"Duplicates appear when the same event is processed more than once."

Another symptom:

"Users occasionally receive two notifications."

Pattern:

"Notification processing is not idempotent."

Patterns provide structure.

Structure makes reasoning easier.

And reasoning makes better design possible.


Draw the Problem

One of the simplest techniques I use when thinking about complicated systems is to draw them.

Not beautifully.

Not professionally.

Just draw them.

Boxes.

Arrows.

Circles.

Labels.

For example:

┌─────────────┐
│ User │
└──────┬──────┘
│
v
┌─────────────┐
│ API │
└──────┬──────┘
│
┌──────────┴──────────┐
v v
┌─────────┐ ┌──────────┐
│Database │ │ External │
│ │ │ Service │
└────┬────┘ └────┬─────┘
│ │
└─────────┬──────────┘
v
┌─────────┐
│ Output │
└─────────┘

Then ask questions.

Where can failure occur?

Where does data change?

Where can latency accumulate?

Where can duplication happen?

Where can state become inconsistent?

Where can a dependency become unavailable?

The drawing gives your brain something to reason about.

The diagram becomes a mental map.


The Shape of Dependencies

Dependencies are another major source of insight.

Imagine:

A → B → C → D

If D becomes unavailable, A might eventually fail.

Now imagine:

┌── B ──┐
A ─────┤ ├──── D
└── C ──┘

Now B and C might provide alternative paths.

The structure has changed.

This is why architecture diagrams are more than documentation.

They are models of dependency.

The shape tells us where the system is tightly connected and where it has separation.

Sometimes the best improvement is not changing a function.

It is changing a relationship.


Learn to Notice Missing Shapes

There is another interesting category of problem.

Sometimes the clue is not what exists.

It is what is missing.

Suppose an application has users, orders, products, payments, and shipments.

But there is no clear representation of an order lifecycle.

Developers keep passing around strings like:

"pending"
"paid"
"processing"
"shipped"

Different parts of the system interpret those values differently.

The missing shape may be a proper domain model.

Or suppose several services independently store information about the same customer.

The problem may be the absence of a shared identity model.

Or suppose every feature implements its own audit logging.

The missing shape might be a centralized event model.

Good engineering sometimes means discovering structures that the system implicitly needs.


Your Brain Builds Models

Software development is partly the art of building mental models.

When you first encounter an unfamiliar system, you have almost no model.

You see files.

Then you discover modules.

Then dependencies.

Then workflows.

Then data structures.

Then business rules.

Eventually, the code stops looking like thousands of individual lines.

You begin seeing the system as a machine.

You can predict where something probably lives.

You can predict which component will be affected by a change.

You can recognize familiar patterns.

That is not magic.

It is accumulated pattern recognition.

The more systems you build, read, debug, and redesign, the more shapes you learn to recognize.


Experience Is Not Just Knowledge

Two developers can know the same programming language and still approach the same problem differently.

One might immediately search for the failing line.

Another might first ask:

What changed?

Another might ask:

What is the data flow?

Another might ask:

Is this state-dependent?

Another might ask:

Is this happening at a system boundary?

Another might ask:

Does this happen only under load?

All of these questions are useful.

The difference is not necessarily syntax knowledge.

It is the ability to frame the problem.

Programming teaches us how to manipulate systems.

Engineering teaches us how to understand them.


The Shape Before the Solution

There is a temptation in software development to jump immediately into implementation.

We see a problem.

We write code.

But sometimes the most valuable engineering work happens before the first line of the solution.

You define the problem.

You identify its boundaries.

You observe its behavior.

You map its dependencies.

You trace its data.

You examine its timing.

You identify repeated patterns.

You understand its states.

Then you choose the solution.

This approach does not make problems disappear.

It makes them easier to reason about.

And that distinction matters.


Build the Habit

The ability to see the shape of a problem can be practiced.

When something breaks, pause.

Ask:

What is the symptom?

What is the underlying behavior?

What is moving?

What is changing?

What is repeated?

What depends on what?

Where are the boundaries?

Does time matter?

Does state matter?

Does scale matter?

What is missing from the current model?

Then draw it.

Even a terrible diagram can reveal something.

A few boxes and arrows can sometimes explain what hundreds of lines of code cannot.


Software Is Full of Shapes

A database has a relational shape.

An API has a request-response shape.

A distributed system has a network shape.

A workflow has a state-transition shape.

A queue has a producer-consumer shape.

A cache has a temporal shape.

A dependency graph has a connectivity shape.

A data pipeline has a flow shape.

A bug has a behavioral shape.

Once you start looking for these structures, software becomes easier to reason about.

You stop seeing isolated events.

You start seeing relationships.

And relationships are where many of the most useful engineering insights live.


The Developer's Sixth Sense

Eventually, you may encounter a strange situation.

You open a codebase.

Nothing is obviously broken.

The tests pass.

The application works.

But something feels structurally unusual.

Maybe one module knows too much.

Maybe data is moving through too many layers.

Maybe several components are solving the same problem.

Maybe a workflow has too many states.

Maybe one dependency sits at the center of everything.

That feeling is not a replacement for evidence.

But it can be the beginning of an investigation.

You have recognized a shape.

Now you test the hypothesis.

You collect logs.

You inspect traces.

You examine queries.

You measure latency.

You read the code.

You reproduce the behavior.

The intuition points toward the question.

Evidence provides the answer.

That is an important relationship between experience and engineering.


Seeing Before Building

The deeper lesson is simple.

Good software engineering begins with seeing.

Before designing the database, see the data.

Before designing the API, see the communication.

Before designing the architecture, see the dependencies.

Before fixing the bug, see the behavior.

Before optimizing the system, see where the work actually happens.

Before creating an abstraction, see the pattern it represents.

A problem is rarely just a sentence in a ticket.

It is usually a system of relationships hiding behind that sentence.

The better you become at seeing those relationships, the more clearly you can reason about what should happen next.

Software is ultimately a collection of structures that interact over time.

And problems are disturbances inside those structures.

Learn to see the movement.

Learn to see the boundaries.

Learn to see the repetition.

Learn to see the dependencies.

Learn to see the state.

Learn to see the time.

Learn to see what is missing.

Eventually, the code becomes more than code.

It becomes a map.

And when you can see the map, you can begin to understand the territory.

Top comments (0)