Reflecting on Five Years of Software Engineering Lessons
Five years of software engineering distilled: fundamentals over frameworks, trust through reliable delivery, code for the next reader, simplicity over clever.

Fundamentals Outlast Frameworks
Every year brings a new framework. The engineers who thrive are the ones who understand the patterns underneath. Frameworks change; data structures, algorithms, networking, and system design thinking stay relevant for decades.
// The framework changes. The pattern doesn't.
// 2020: Express middleware
app.use((req, res, next) => {
const start = Date.now();
res.on("finish", () => {
console.log(`${req.method} ${req.path} - ${Date.now() - start}ms`);
});
next();
});
// 2023: Next.js middleware
export function middleware(request: NextRequest) {
const start = Date.now();
const response = NextResponse.next();
response.headers.set("X-Response-Time", `${Date.now() - start}ms`);
return response;
}
// 2025: Whatever comes next — still request interception,
// still wrapping the handler, still measuring duration.
// The framework is syntax. The pattern is knowledge.Code Is Read More Than It Is Written
Clever code impresses no one six months later when it needs debugging at 3 AM. Writing for readability is not a sign of weakness—it is a sign of experience.
// ❌ Clever — what does this do at a glance?
const r = d.filter((x) => x.s === "a" && x.t > Date.now() - 864e5).reduce(
(a, c) => ({ ...a, [c.c]: (a[c.c] || 0) + c.v }),
{} as Record<string, number>
);
// ✅ Clear — intention is obvious without comments
const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
const activeTransactions = deposits.filter(
(deposit) =>
deposit.status === "active" && deposit.timestamp > oneDayAgo
);
const totalsByCategory = activeTransactions.reduce<Record<string, number>>(
(totals, transaction) => ({
...totals,
[transaction.category]:
(totals[transaction.category] ?? 0) + transaction.value,
}),
{}
);Ship Small, Ship Often
Large pull requests sit in review for days, accumulate merge conflicts, and ship with hidden bugs. Small, focused changes get reviewed in minutes and deploy with confidence.
// Deployment metrics that tell the story
interface DeliveryMetrics {
// Small PRs: reviewed faster, fewer bugs
avgPRSize: { lines: number; reviewTimeHours: number; bugRate: number };
// Deployment frequency: more deploys = smaller blast radius
deploysPerWeek: number;
// Lead time: commit to production
leadTimeHours: number;
// Recovery: how fast you fix things when they break
meanTimeToRecoveryMinutes: number;
}
// What I've seen across teams:
const highPerformingTeam: DeliveryMetrics = {
avgPRSize: { lines: 150, reviewTimeHours: 2, bugRate: 0.02 },
deploysPerWeek: 25,
leadTimeHours: 4,
meanTimeToRecoveryMinutes: 30,
};
const strugglingTeam: DeliveryMetrics = {
avgPRSize: { lines: 2000, reviewTimeHours: 72, bugRate: 0.15 },
deploysPerWeek: 1,
leadTimeHours: 336, // 2 weeks
meanTimeToRecoveryMinutes: 480, // 8 hours
};
// The difference is not talent. It's workflow.Tests Save Time — Eventually
Writing tests feels slow until the first time a test catches a regression that would have reached production. The ROI is not immediate, but it compounds.
// The test that pays for itself
describe("checkout price calculation", () => {
it("applies percentage discount before tax", () => {
const result = calculateTotal({
items: [{ price: 100, quantity: 2 }],
discount: { type: "percentage", value: 10 },
taxRate: 0.08,
});
// Without this test, a refactor swapped tax and discount order.
// That bug would have overcharged every customer by ~1%.
// This test caught it in CI, saved a production incident,
// and justified every minute spent writing it.
expect(result.subtotal).toBe(200);
expect(result.discountAmount).toBe(20);
expect(result.tax).toBe(14.4); // 8% of $180
expect(result.total).toBe(194.4);
});
});The Best Architecture Is the Simplest One That Works
Over-engineering is the most common mistake in mid-career. A monolith handles more traffic than you think. Microservices solve organizational problems, not technical ones—until you have the team size to justify the operational overhead.
// Year 1 thinking: "We need microservices for scalability"
// Year 3 reality: "We have 40 services, 3 engineers, and
// no one understands the full system"
// ❌ Premature microservices for a small team
// services/user-service/
// services/order-service/
// services/payment-service/
// services/notification-service/
// services/inventory-service/
// + Kubernetes, service mesh, distributed tracing, 5 databases
// ✅ Modular monolith — same boundaries, fraction of the complexity
// src/modules/users/
// src/modules/orders/
// src/modules/payments/
// src/modules/notifications/
// src/modules/inventory/
// + One database, one deployment, clear module boundaries
// Extract to services WHEN you have the team and traffic to justify itCommunication Scales; Code Doesn't
The impact ceiling for pure coding is lower than you think. The engineers who shape products, influence architecture, and accelerate their teams are the ones who communicate effectively—in design docs, code reviews, incident retrospectives, and daily conversations.
// A pull request description that accelerates review
interface EffectivePRDescription {
what: string; // What changed
why: string; // Why this approach
how: string; // Key implementation decisions
testing: string; // How you verified it works
risks: string; // What could go wrong
rollback: string; // How to undo if needed
}
// vs. the PR description we've all seen:
// Title: "fix stuff"
// Description: ""
// Files changed: 47
// The first one gets reviewed in 20 minutes.
// The second one sits for 3 days.Invest in Developer Experience
The tools, scripts, and workflows you build for yourself compound daily. A 30-second improvement to your development loop saves hours over a year.
// Small investments that compound
const developerExperienceWins = [
{
investment: "Hot reload configured properly",
timePerOccurrence: "10 seconds saved",
frequency: "200 times/day",
annualSavings: "~110 hours/year",
},
{
investment: "One-command database reset with seed data",
timePerOccurrence: "5 minutes saved",
frequency: "3 times/week",
annualSavings: "~13 hours/year",
},
{
investment: "Pre-commit hooks catching lint/type errors",
timePerOccurrence: "15 minutes saved (no CI round-trip)",
frequency: "5 times/week",
annualSavings: "~65 hours/year",
},
{
investment: "Automated PR template with checklist",
timePerOccurrence: "3 minutes saved",
frequency: "10 times/week",
annualSavings: "~26 hours/year",
},
];The Lesson Underneath All the Lessons
Software engineering is not about writing perfect code. It is about solving problems for people—users, teammates, and your future self—while navigating constraints of time, knowledge, and complexity. The best engineers are not the ones who know the most technologies. They are the ones who consistently make good tradeoffs, communicate clearly, and leave the codebase better than they found it.
Five years, hundreds of pull requests, dozens of production incidents, and the most important skill turned out to be judgment: knowing when to build, when to buy, when to simplify, and when to ship. The code is just the artifact. The thinking is what matters.


