One of the most valuable things software engineering has taught me is this:
The code I was most proud of five years ago is probably the code I would rewrite today.
Not because it was bad.
Not because it failed.
But because experience changes how you see systems.
Earlier in my career, I believed software engineering was mostly about writing code.
Today, I think it's mostly about making decisions.
Every project leaves behind lessons.
Some come from successful launches.
Others come from production incidents at two in the morning.
Some come from scaling problems.
Others come from maintaining code you wrote years ago.
Every application becomes a teacher.
Looking back at the systems I've built, I don't regret them.
I'm grateful for them.
They taught me exactly what I'd build differently today.
I Would Design the Architecture Before the Features
One of my earliest mistakes was thinking about features first.
Users can register.
Users can log in.
Users can upload files.
Users can purchase products.
These were useful goals.
But they weren't architectural goals.
Today I start somewhere else.
How will information flow?
Where does business logic belong?
Which components should know about each other?
How will this system grow in three years?
Features change.
Architecture remains.
From Requests to Information Flow
Every application is really a collection of information moving through a system.
Once I understood that, software became much easier to reason about.
Instead of asking:
"How do I implement login?"
I now ask:
"Where does authentication fit inside the architecture?"
Instead of asking:
"How do I upload images?"
I ask:
"How should media move through the system?"
The questions became architectural instead of functional.
The Architecture I'd Build Today
Instead of tightly coupling everything together, I'd organize the application into clear layers.
Modern Application Architecture
Client (Web / Mobile)
│
▼
REST / GraphQL API
│
▼
Authentication Middleware
│
▼
Application Services
┌───────────┴───────────┐
▼ ▼
Domain Logic Background Jobs
│ │
▼ ▼
Repository Layer Event Queue
│ │
▼ ▼
PostgreSQL / Redis Email / Analytics
│
▼
Monitoring & Logging
Every layer has one responsibility.
The client presents information.
The API receives requests.
Services contain business logic.
Repositories manage persistence.
Queues handle asynchronous work.
Monitoring observes everything.
The architecture tells the story before the code does.
I Would Separate Business Logic Earlier
Earlier versions of my projects often placed too much logic inside controllers.
It worked.
Until it didn't.
Controllers became larger.
Testing became harder.
Features became interconnected.
Today I'd keep controllers incredibly small.
Their job is simple.
Receive a request.
Validate it.
Call a service.
Return a response.
The real intelligence belongs elsewhere.
A Simpler Service Layer
Instead of mixing everything together, I'd write services that solve one business problem.
pub struct OrderService {
repository: OrderRepository,
events: EventBus,
}
impl OrderService {
pub async fn create_order(
&self,
order: Order
) -> Result<Order> {
let saved = self.repository
.save(order)
.await?;
self.events.publish(
OrderCreated {
id: saved.id
}
).await?;
Ok(saved)
}
}
Notice what doesn't happen here.
No emails.
No notifications.
No analytics.
No inventory updates.
Those belong to independent services reacting to events.
Small responsibilities create maintainable systems.
I'd Embrace Events From Day One
One lesson experience taught me is that applications should communicate through events whenever possible.
Instead of this:
Order Service
│
▼
Send Email
│
▼
Update Inventory
│
▼
Generate Invoice
│
▼
Notify Analytics
I'd build this instead:
Order Created Event
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Email Service Inventory Service Analytics
│ │ │
▼ ▼ ▼
Independent Independent Independent
Nothing depends directly on anything else.
Adding a new feature becomes publishing another subscriber.
Architecture becomes extensible instead of fragile.
I'd Invest More in Observability
Earlier in my career I logged errors.
Today I monitor systems.
Those are different things.
Modern software should answer questions automatically.
How many requests failed?
Which endpoint is slow?
Why is memory increasing?
Which customer experienced an error?
Monitoring transforms debugging from guessing into understanding.
I'd build dashboards before launching products.
Caching Would Be Part of the Design
Caching used to be something I added later.
Now I consider it during architecture.
User Request
│
▼
API Gateway
│
Cache Available?
│ │
Yes No
│ │
▼ ▼
Return Cached Query Database
Response │
▼
Store in Cache
│
▼
Return Response
Fast systems rarely rely only on faster databases.
They avoid unnecessary work.
I Would Think More About Failure
One question changed how I design systems.
"What happens if this component fails?"
Earlier versions of my software assumed success.
Modern systems assume failure.
Databases disconnect.
Networks timeout.
Caches disappear.
Queues overflow.
Users refresh pages.
Failures become architectural considerations instead of unexpected events.
Testing Would Begin Earlier
I once viewed testing as something performed after development.
Now I see testing as part of design.
When software is difficult to test...
It's often difficult to understand.
Writing testable software usually produces modular software.
The tests become documentation.
Future developers—including my future self—benefit.
Databases Deserve More Respect
Earlier projects treated the database like storage.
Today I see it as architecture.
Tables define relationships.
Indexes define performance.
Constraints preserve integrity.
Transactions preserve consistency.
A well-designed schema quietly improves every layer above it.
Poor schemas spread complexity everywhere.
I'd Build APIs Like Products
An API isn't simply an interface.
It's a contract.
Clients depend on it.
Future developers depend on it.
Documentation depends on it.
Consistency matters.
Versioning matters.
Naming matters.
Clear APIs reduce complexity before code executes.
Security Would Be Built In
Earlier projects often added security after features worked.
Now security becomes part of architecture.
Authentication.
Authorization.
Rate limiting.
Input validation.
Encryption.
Audit logging.
Security should never feel bolted on.
It should feel inevitable.
I'd Optimize for Maintenance
Earlier I optimized for finishing.
Today I'd optimize for maintaining.
Future developers matter.
Future teammates matter.
Future Derek matters.
Readable code.
Simple naming.
Predictable architecture.
Good documentation.
Maintenance is where software spends most of its life.
Continuous Delivery Over Perfect Releases
Another lesson took years to appreciate.
Shipping once isn't success.
Shipping continuously is.
I'd automate deployments.
Automate testing.
Automate monitoring.
Automate rollbacks.
Automation reduces human error.
Consistency scales better than heroics.
Software Is Never Finished
One belief changed everything for me.
Applications are living systems.
Users change.
Businesses evolve.
Technology advances.
Requirements shift.
Software must evolve alongside them.
Architecture should welcome change instead of resisting it.
Experience Changed My Priorities
Years ago, I admired clever code.
Today I admire understandable systems.
Years ago, I chased elegant algorithms.
Today I chase clear architecture.
Years ago, I celebrated large features.
Today I celebrate fewer responsibilities.
Experience didn't make me write more code.
It made me write less.
But with greater intention.
Final Thoughts
If I could start every project over again, I wouldn't begin by choosing a framework.
I wouldn't begin by designing database tables.
I wouldn't even begin by writing code.
I'd begin by understanding the problem deeply.
Then I'd design how information should move through the system.
I'd define clear boundaries between responsibilities.
I'd think about failure before success.
I'd build observability alongside functionality.
I'd treat APIs as long-term contracts.
I'd embrace events over tight coupling.
I'd automate everything that could be automated.
I'd optimize not only for today's requirements, but for the developer who will maintain the code years from now.
Ironically, I don't think I'd build dramatically different software.
Users might never notice the difference.
The interfaces would look similar.
The features would behave the same.
But underneath, the architecture would tell a very different story.
A story of loose coupling instead of dependency.
Clarity instead of cleverness.
Feedback instead of guesswork.
Maintainability instead of shortcuts.
And perhaps that's the greatest lesson software engineering has taught me.
The goal isn't to build software that impresses people on the day it's launched.
The goal is to build software that still makes sense years later, when requirements have changed, teams have grown, and someone—quite possibly your future self—has to understand every decision you made.
If there's one thing I'd build differently today, it isn't just the code.
It's the thinking behind the code.
Because in the end, architecture is simply experience made visible.
Top comments (0)