DEV Community

Cover image for The Code Worked. Then I Started Asking What Happens Next.
Okello Odhiambo
Okello Odhiambo

Posted on

The Code Worked. Then I Started Asking What Happens Next.

The Code Worked. Then I Started Asking What Happens Next.

In the first part of this series, I wrote about a change taking place in how I approach software development at Zone01 Kisumu.

I am becoming less satisfied with knowing that code works.

I want to understand what happens around the code.

That has pushed me towards architecture.

For a long time, "software architecture" sounded like something that happened much later in a developer's career.

I imagined enormous system diagrams, microservices, Kubernetes clusters and people with "Architect" in their job titles deciding where everything should go.

My experience with projects is giving me a much more practical definition.

Architecture is what your decisions become when the project grows.

Every small decision leaves a footprint

Imagine I need user information.

The fastest solution might be to fetch it directly inside whatever component currently needs it.

Then another component needs it.

Then another.

Soon, three parts of the application fetch the same information differently.

One handles errors.

One doesn't.

One caches the result.

One expects a slightly different response.

None of those individual decisions seemed architectural when they were made.

Together, they have created an architecture.

This has been an important realization for me.

We don't avoid architecture by refusing to think about it.

We simply end up with architecture we didn't deliberately choose.

Start with the problem, not the technology

One temptation I have noticed in software development is beginning with technology.

"I'll use React."

"I'll use PostgreSQL."

"I'll use microservices."

"I'll add Redis."

"I'll containerize everything."

But those statements don't explain the problem.

The better questions come first.

What am I building?

Who will use it?

What information does it handle?

How frequently will that information change?

Which operations are critical?

What happens if a service becomes unavailable?

Does this actually need real-time communication?

What security boundaries exist?

What is the simplest design capable of meeting those requirements?

Only then do technology choices become meaningful.

If the problem requires structured relationships and transactions, that tells me something about storage.

If users are uploading large files, that tells me something different.

If a process can take several minutes, perhaps it shouldn't hold an HTTP request open.

Architecture starts with constraints.

Technology responds to them.

Following the data has helped me understand systems

One technique I increasingly find useful is tracing a piece of data from beginning to end.

Take something ordinary:

A user changes their profile picture.

It looks like one feature.

But follow the data.

User selects image
        ↓
Browser
        ↓
HTTP request
        ↓
Authentication
        ↓
File validation
        ↓
Storage
        ↓
Database reference
        ↓
API response
        ↓
Updated interface
Enter fullscreen mode Exit fullscreen mode

Now the questions become clearer.

What file types are accepted?

What is the maximum size?

Do we trust the extension?

Where is the actual image stored?

Is it public or private?

What happens to the previous image?

Who can retrieve it?

What happens if storage succeeds but updating the database fails?

What happens if the user loses their connection?

The diagram isn't the architecture.

The decisions behind every arrow are.

Payments make the lesson even clearer

Suppose the application accepts online payments.

From the user's perspective:

Click Pay → Confirm → Success
Enter fullscreen mode Exit fullscreen mode

From the system's perspective:

Client
   ↓
Application API
   ↓
Payment provider
   ↓
External payment network
   ↓
Payment confirmation
   ↓
Webhook
   ↓
Verification
   ↓
Database
   ↓
Order / Subscription
Enter fullscreen mode Exit fullscreen mode

Now architecture directly affects money.

What is the source of truth for whether somebody paid?

The browser?

Definitely not.

The payment provider?

How do we verify its callback?

What if the callback arrives twice?

What if payment succeeds but our server is temporarily unavailable?

What if the amount received differs from the amount expected?

What if two requests try to update the same transaction?

These aren't syntax questions.

I could ask AI to generate a payment endpoint and receive one almost immediately.

The difficult part is deciding whether I should trust what it generated.

AI makes architecture more important, not less

AI has reduced the friction involved in creating software.

That is powerful.

It also means we can create bad architecture faster.

I can ask for:

Add Redis caching.

And get Redis.

But should this system have Redis?

I can say:

Convert this application to microservices.

And receive a directory containing several services.

But what problem did microservices solve?

I can request:

Make this scalable.

And receive queues, containers, caching layers and abstractions.

But complexity isn't scalability.

Sometimes the better architecture is:

Frontend
    ↓
Backend
    ↓
PostgreSQL
Enter fullscreen mode Exit fullscreen mode

And that's it.

A system doesn't become professional because its diagram contains more boxes.

Every additional component creates something else to deploy, secure, observe, update, debug and pay for.

One lesson I am carrying with me is:

Complexity should have to justify its existence.

Separation of concerns finally started making practical sense

"Separation of concerns" is one of those phrases developers encounter repeatedly.

It made much more sense once I started seeing what happens when we ignore it.

Suppose an HTTP handler:

  • validates input,
  • checks permissions,
  • calculates prices,
  • queries the database,
  • sends emails,
  • uploads files,
  • formats responses.

Technically, it can work.

But now changing one business rule means touching the same code responsible for several unrelated things.

Testing becomes difficult.

Reusing logic becomes difficult.

Understanding failure becomes difficult.

The better question becomes:

What should this part of the system know?

A route should know about HTTP.

Business logic should know about business rules.

A repository should know how data is persisted.

A payment integration should know how to communicate with the payment provider.

That separation isn't about making the folder structure look impressive.

It creates boundaries around change.

Go has made boundaries visible to me

Go has been useful here because packages force me to think about what belongs together.

If everything imports everything else, something is probably wrong.

If changing one package causes unrelated parts of the application to break, the boundaries probably need reconsideration.

If a function requires ten dependencies just to do one thing, maybe it is doing more than one thing.

Python and JavaScript present the same architectural questions in different forms.

That is one benefit of learning several languages.

The syntax changes.

The underlying engineering problems keep returning.

Data still needs boundaries.

Errors still need handling.

Dependencies still need management.

Users still do unexpected things.

Networks still fail.

Databases still need consistency.

Security still matters.

Architecture is also about failure

Something else has changed in my thinking.

I used to design mainly around the successful path.

User submits form
→ Server processes it
→ Database saves it
→ Success
Enter fullscreen mode Exit fullscreen mode

Now I try to spend more time on the arrows.

What happens if validation fails?

What happens if the database is unavailable?

What if the request is repeated?

What if only half of an operation succeeds?

What if an external API times out?

What if the user refreshes the page?

What if somebody deliberately sends malformed data?

The happy path shows that a feature can work.

The failure paths tell me whether I have engineered it.

Maintainability means thinking about another human

Architecture is often discussed in terms of machines:

performance, memory, throughput, latency.

But maintainability is largely about people.

Could another developer understand this?

Could someone safely change it?

Does the naming reveal intent?

Are the dependencies obvious?

Can we test one part without starting the entire application?

Can somebody joining the project understand how data moves through it?

Peer-to-peer learning at Zone01 makes that very tangible.

Code isn't only something I communicate to a computer.

It is something I communicate to the next developer.

Sometimes that developer is me three weeks later wondering why I made a particular decision.

Good architecture doesn't predict the future

I no longer think architecture means anticipating every possible requirement.

That can lead directly to overengineering.

Instead, I am beginning to think about it as making today's decisions without unnecessarily trapping tomorrow's developer.

Build what the project needs.

Create clear boundaries.

Keep responsibilities understandable.

Protect important data.

Test critical behaviour.

Document decisions that aren't obvious.

And leave room for the system to change.

The objective isn't to build a system that never needs modification.

Software that matters will change.

The objective is to make change survivable.

That raises the final question in this series.

If AI can increasingly generate our components, endpoints, tests and even entire applications, where should developers invest their effort?

For me, the answer is becoming clearer.

Our value moves further upstream—from producing code to understanding problems, evaluating trade-offs and taking responsibility for the systems we put into the world.

That is where I want to finish this series.

Part 2 of 3 — Learning Software Engineering in the Age of AI

Top comments (0)