DEV Community

Cover image for The Things I Only Learned After Shipping 20+ Real Projects
Derek mwale
Derek mwale

Posted on

The Things I Only Learned After Shipping 20+ Real Projects

When I wrote my first real application, I thought the hardest part of software engineering was getting the code to compile.

Then I thought it was fixing bugs.

Later I thought it was learning new frameworks.

Today, after shipping more than twenty real projects—some successful, some abandoned, some open-sourced, some rewritten entirely—I think the hardest part of software engineering is something else.

It is learning how to make good engineering decisions.

Every project leaves behind code.

But more importantly, every project leaves behind lessons.

Some lessons arrive through success.

Others arrive through painful mistakes.

A few arrive months after deployment when you realize that the architecture you proudly designed now slows down every new feature.

Experience has a way of changing your priorities.

Earlier in my career, I optimized for writing more code.

Today, I optimize for writing better systems.

Looking back, there are many things I wish someone had told me before I started building production software.

Ironically, I probably wouldn't have understood them until I experienced them myself.


Software Is Never Finished

One of the biggest misconceptions I had was believing projects eventually become "done."

Reality looks very different.

Every application becomes a living system.

Users request new features.

Businesses change direction.

Dependencies receive updates.

Security vulnerabilities appear.

Performance expectations increase.

Operating systems evolve.

Browsers change.

Cloud providers introduce new services.

The software never stops moving.

The goal isn't to finish software.

The goal is to build software that can continue evolving.

That realization completely changed how I design systems.


Architecture Matters More Than Algorithms

University teaches algorithms.

Production teaches architecture.

I've rarely encountered applications that failed because they used the wrong sorting algorithm.

I've encountered many that struggled because responsibilities weren't clearly separated.

Business logic lived inside controllers.

Authentication spread across multiple services.

Database access appeared everywhere.

Dependencies became tangled.

Architecture quietly determines how easy software becomes to change.


Every Request Tells a Story

I no longer think about API endpoints individually.

I think about journeys.

```text id="r7n0av"
User Request


Load Balancer


Authentication


API Controller


Business Service
│ │
▼ ▼
Redis Cache PostgreSQL
│ │
└──────┬──────┘

JSON Response




Every layer contributes.

When something becomes slow, the problem usually isn't one function.

It's the interaction between many components.

---

# Simplicity Ages Better

Earlier versions of my software often contained clever abstractions.

Generic builders.

Complicated inheritance.

Nested dependency chains.

They felt impressive.

Six months later they became difficult to understand.

Today I choose clarity instead.

Simple functions.

Predictable naming.

Small services.

Less abstraction.

Future maintainability matters more than present elegance.

---

# Databases Deserve More Respect

One lesson took years to appreciate.

The database isn't just where information lives.

It defines relationships.

Performance.

Consistency.

Integrity.

Good schemas simplify applications.

Poor schemas spread complexity throughout the entire codebase.

Indexes often improve performance more than rewriting application code.

Constraints eliminate entire categories of bugs.

The database quietly influences every request.

---

# Caching Is Architecture

Earlier I viewed Redis as an optimization.

Now I think of caching as part of system design.



```text id="x8h53m"
          Client Request
                 │
                 ▼
            Redis Cache
             │       │
           Hit      Miss
            │        │
            ▼        ▼
      Return Data Database
                     │
                     ▼
              Store Cache
                     │
                     ▼
               Return Data
Enter fullscreen mode Exit fullscreen mode

Good caching isn't about speed alone.

It's about reducing unnecessary work.

The fastest query is often the one you never execute.


Events Changed How I Build Software

One architectural lesson transformed the way I think about large systems.

Instead of letting one service perform every responsibility, I let services communicate through events.

```text id="s4cvny"
User Registered

┌───────┼────────┐
▼ ▼ ▼
Email Analytics CRM Sync




The registration service no longer knows how emails work.

Or analytics.

Or customer relationship management.

It simply announces that something happened.

Other services react independently.

The architecture becomes remarkably flexible.

---

# Thin Controllers Saved Me Countless Hours

Earlier controllers handled everything.

Validation.

Business rules.

Emails.

Database logic.

Notifications.

Today my controllers barely do anything.



```rust id="fx1lh3"
pub async fn register(
    request: RegisterRequest,
    service: UserService,
) -> Result<ApiResponse<User>> {

    let user =
        service.register(request).await?;

    Ok(ApiResponse::success(user))
}
Enter fullscreen mode Exit fullscreen mode

The service contains the actual business logic.

```rust id="q0m1ye"
impl UserService {

pub async fn register(
    &self,
    request: RegisterRequest,
) -> Result<User> {

    let user =
        self.repository
            .create(request)
            .await?;

    self.events.publish(
        UserRegistered {
            id: user.id
        }
    ).await?;

    Ok(user)
}
Enter fullscreen mode Exit fullscreen mode

}




Small responsibilities produce maintainable systems.

---

# Monitoring Beats Guessing

Logging tells you what happened.

Monitoring tells you what's happening.

Earlier I waited for users to report bugs.

Today dashboards often identify problems before users notice them.

CPU usage.

Memory consumption.

Database latency.

Cache hit rate.

Request throughput.

Error percentages.

Observability transforms debugging into engineering.

---

# Testing Changes Design

One unexpected lesson surprised me.

Difficult-to-test code often indicates difficult-to-maintain architecture.

When services become modular...

Testing becomes easier.

When responsibilities become focused...

Unit tests become smaller.

Testing improves design almost as much as it verifies behavior.

---

# Failure Is Part of the Architecture

Production eventually teaches every engineer the same lesson.

Everything fails.

Networks timeout.

Servers restart.

Caches disappear.

Third-party APIs become unavailable.

Instead of asking:

"Will this fail?"

I now ask:

"How gracefully can it recover?"



```text id="c6fjvk"
 External API
      │
Available?
 │       │
Yes      No
 │        │
 ▼        ▼
Proceed  Retry
            │
            ▼
      Cached Result
            │
            ▼
       Log Failure
Enter fullscreen mode Exit fullscreen mode

Reliable software anticipates failure instead of pretending it won't happen.


Documentation Saves Future You

One of my favorite engineers once said:

"Documentation is a gift to your future self."

I didn't appreciate that enough.

Months later, opening an old project without documentation feels like exploring an unfamiliar city.

Clear architecture diagrams.

API examples.

Database relationships.

Deployment instructions.

These reduce future confusion enormously.


Shipping Changes How You Learn

Tutorials teach techniques.

Production teaches judgment.

Books explain principles.

Real systems explain trade-offs.

Every decision becomes contextual.

Should we cache this?

Should we normalize this table?

Should this service own authentication?

Should this operation be asynchronous?

Experience teaches there is rarely one perfect answer.

Only thoughtful trade-offs.


Growth Changes Priorities

Earlier I cared about learning more programming languages.

Today I care more about designing better systems.

Languages matter.

Architecture matters more.

Frameworks evolve.

Engineering principles endure.

Clear responsibilities.

Loose coupling.

Simple interfaces.

Reliable monitoring.

Good naming.

Those ideas remain valuable regardless of technology.


Software Is Communication

Perhaps the biggest lesson surprised me.

Software isn't only communication between computers.

It's communication between engineers.

Naming communicates.

Architecture communicates.

Folder structures communicate.

API responses communicate.

Comments communicate.

The next engineer should understand your intentions without needing to ask.

Sometimes that next engineer is you.


Experience Made Me Slower—and Better

Interestingly, I write code more slowly today than I did years ago.

Not because I've forgotten anything.

Because I think longer before typing.

I sketch architectures.

Question assumptions.

Imagine future requirements.

Consider failure scenarios.

Good engineering begins long before implementation.


Final Thoughts

Looking back at more than twenty real projects, I don't remember every feature I built.

I don't remember every framework version.

I don't remember every library I used.

But I remember the lessons.

I remember the production bugs that revealed architectural weaknesses.

I remember the APIs that became difficult to evolve because I hadn't thought about versioning.

I remember the databases that taught me the value of proper indexing.

I remember the monitoring dashboards that identified issues before customers did.

I remember the projects that became easier to maintain because responsibilities were clearly separated.

Most importantly, I remember realizing that software engineering is far less about writing code than I once believed.

Code is only one expression of engineering.

Architecture.

Data modeling.

Caching.

Observability.

Documentation.

Testing.

Reliability.

Maintainability.

Those are equally important.

If I could go back and give my younger self one piece of advice before shipping the first project, it would be this:

Don't optimize for finishing software.

Optimize for living with it.

Because software stays with you far longer than the excitement of launching it.

Every design decision eventually returns.

Sometimes as technical debt.

Sometimes as simplicity.

Sometimes as confidence.

And after enough projects, you begin to realize that experience isn't measured by how many applications you've built.

It's measured by how many engineering lessons those applications quietly taught you along the way.

Top comments (0)