In 2009, I was fresh out of the gate. I had plenty of energy, but very little wisdom.
My toolkit was simple: FileZilla, jQuery, and a lot of blind faith. I genuinely believed that if it worked on my laptop, the job was done.
Then came the day that taught me otherwise.
I deployed an e-commerce feature on a Friday afternoon without load testing. It worked fine for me. But on Monday morning, under real traffic, a bad database query locked the entire system.
I spent four hours sweating while support tickets piled up.
I wasn't a bad developer. I was just inexperienced. I knew the syntax, but I didn't understand the system. There is a massive gap between writing code that runs and engineering software that survives.
If you are early in your journey, don't fear these moments. They are the price of entry. It took me a decade to bridge the gap between 'coder' and 'engineer.'
You can cross it faster. Here are the 10 shifts that changed my career.
1. Don't block the user (async processing)
I used to handle heavy tasks, like generating a PDF or importing a CSV, right inside the main HTTP request. The browser would spin for 40 seconds, and if the user got bored and closed the tab, the process died.
I didn’t realize I was blocking the user from doing anything else.
How I handle it today: I treat the API like a receptionist, not a factory worker. It takes the order, acknowledges it, and moves on.
Senior code example: asynchronous API handling
// 1. Add to queue (Redis/SQS)
await reportQueue.add('pdf-gen', { userId: req.user.id });
// 2. Release the user immediately
return res.status(202).json({ msg: "We are working on it." });
This keeps the application responsive, no matter how heavy the workload is.
2. Zero trust architecture (validation)
I used to be naive about the Frontend. I thought: "If I put type='email' in the HTML input, I don't need to validate it on the server."
I was wrong. A malicious user can bypass your UI with a simple curl command and send garbage (or exploits) to your database.
Senior code example: Backend data validation with Zod
import { z } from 'zod'; // Assuming zod is installed
const UserSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
});
// If this fails, the code stops here, ensuring data integrity.
const cleanData = UserSchema.parse(req.body);
The Backend is the guardian of data integrity. Never lower the shields just because the UI looks safe.
3. The N+1 problem (database efficiency)
I often wrote code that looked logical in the editor but was a disaster in production. I would fetch a list of posts and then loop through them to fetch the author.
In code, it looks fine. In reality, for 50 posts, I was triggering 51 separate database queries.
Senior code example: efficient SQL Join
SELECT p.title, u.name
FROM posts p
JOIN users u ON p.author_id = u.id;
Latency kills user experience. Reduce the round trips.
4. Logs are for context, not text
console.log("Error happened") is useless when you have 500 concurrent users. You need to know who crashed and why.
I stopped writing text logs and started writing structured logs (JSON).
Senior code example: structured logging
// Assuming a logger like Winston or Pino is configured
logger.error({
msg: "Payment failed",
userId: req.user.id,
cartTotal: req.body.total,
reason: err.message
});
Now, instead of guessing, you can search your log aggregator for userId: 123 and see exactly what happened to that specific customer.
5. Config goes in the environment
I used to hardcode API keys and database passwords directly in the code. It was "easier". Until I accidentally committed a password to a public repo and had to rotate every key we had.
The Rule: code is logic. Configuration is environment.
Senior code example: Environment variables for configuration
// The code doesn't know the secret. It asks the environment.
const dbPassword = process.env.DB_PASS;
This makes your application secure and portable. You can move from Staging to Production without touching a single line of code.
6. The network is unreliable (retry logic)
I assumed APIs would always be online. If a request failed, I just threw an error to the user. But networks glitch. Packets get lost. A 50ms blip shouldn't break your app.
Now, I implement Exponential backoff. If it fails, wait 200ms and try again. Then 500ms. Then stop.
Senior code example: fetch with exponential backoff Retry
import axios from 'axios'; // Assuming axios is installed
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function fetchWithRetry(url) {
for (let i = 0; i < 3; i++) { // Max 3 retries
try {
return await axios.get(url);
} catch (e) {
console.warn(`Request to ${url} failed, retrying in ${200 * Math.pow(2, i)}ms...`);
await sleep(200 * Math.pow(2, i)); // Exponential backoff
}
}
throw new Error(`Failed to fetch ${url} after multiple retries.`);
}
// Example usage:
// fetchWithRetry('https://api.example.com/data')
// .then(response => console.log(response.data))
// .catch(error => console.error(error.message));
Robust systems absorb small failures instead of passing them to the user.
7. Cache the heavy stuff
I used to hit the database for data that rarely changed, like the list of product categories or site settings. It was a waste of resources.
The solution: Check memory (Redis) first. Check the database second.
Senior Code example: caching with redis
// Assuming redis client and db client are initialized
let categories = await redis.get('categories');
if (!categories) {
categories = await db.query('SELECT * FROM categories');
// Cache for 1 hour (3600 seconds)
await redis.set('categories', JSON.stringify(categories), 'EX', 3600);
} else {
categories = JSON.parse(categories);
}
Protect your primary database. It is the hardest part of your stack to scale.
8. Cookies over localstorage
I stored JWT tokens in localStorage because it was easy. But any JavaScript code (including third-party analytics libraries) can read localStorage.
I switched to HttpOnly cookies.
Senior code example: secure HttpOnly cookies
// When setting the cookie in a Node.js (Express) app
res.cookie('auth', token, {
httpOnly: true, // JavaScript cannot read this, mitigating XSS
secure: process.env.NODE_ENV === 'production', // Only send over HTTPS in production
sameSite: 'Lax', // Protection against CSRF attacks
maxAge: 3600000 // 1 hour expiration
});
This simple change eliminates a huge class of XSS attacks. It might be slightly annoying to set up, but it is worth the security boost.
9. Infrastructure as code
I used to configure servers manually via SSH. If the server died, I had to spend hours setting up a new one from memory.
Now, I use Docker. I define the environment in a file.
Senior code example: basic dockerfile
# Use a Node.js base image
FROM node:18-alpine
# Set the working directory in the container
WORKDIR /app
# Copy package.json and package-lock.json to install dependencies
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Expose the port your app runs on
EXPOSE 3000
# Command to run the application
CMD ["node", "index.js"]
If a server dies, an automated process kills it and spins up a new clone in seconds. No emotional attachment required.
10. Focus on the solution, not the code
This was the hardest shift. If a client asked for an "Export to Excel" button, I built it.
Today, I ask "Why?". Often, they want the Excel file to import data into another system. So I suggest: "Let's integrate directly with that system via API."
The lesson: A junior developer builds what is asked. A senior engineer solves the underlying problem.
The bottom line
Seniority is not about how many years you have been coding. It is about how much you care about what happens after the deploy.
It is about moving from "it works" to "it lasts".
I'm currently drafting Part 2, focusing on the messy stuff: heavy debugging and production scars.
But first, I want to hear from you:
Which of these lessons did you learn the hard way?
Your story might save someone from a 2 AM incident. 👇

Top comments (0)