Back to articles

From Junior to Senior: Lessons Learned in My First 3 Years

Practical lessons from my first three years as a software engineer—impact, communication, code quality, and ownership—with examples you can apply today.

Dec 1, 20256 min read
CareerTools
From Junior to Senior: Lessons Learned in My First 3 Years
📝
Meta description: Practical lessons from my first three years as a software engineer—impact, communication, code quality, and ownership—with examples you can apply today.

From Junior to Senior: Lessons Learned in My First 3 Years

Breaking into the industry felt like sprinting into a marathon. In my first three years, I learned that leveling up is less about knowing every framework and more about compounding small professional habits: communicating clearly, writing maintainable code, testing with intention, and focusing on business impact. This post distills the practices that moved me from task‑taker to trusted engineer.


1) Impact over output

Early on, I equated productivity with number of tickets closed. Senior engineers optimize for business outcomes: fewer incidents, faster load times, higher conversion, lower costs.

Find the smallest meaningful win

Instead of rebuilding a service, I reduced cold‑start latency by caching config and lazy‑loading non‑critical deps. The result cut p95 request time by 28% with one week of work.

// before: config loaded and parsed on every request
app.get('/price', async (req, res) => {
  const config = await loadConfig();
  const price = computePrice(config, req.query);
  res.json({ price });
});

// after: config cached and refreshed on interval
let cachedConfig: Config | null = null;
async function getConfig() {
  if (!cachedConfig) cachedConfig = await loadConfig();
  return cachedConfig;
}
setInterval(async () => (cachedConfig = await loadConfig()), 5 * 60_000);

Practical tip

  • Ask “what metric changes if I ship this?” If none, reconsider the task or reframe it.

2) Communicate early, write things down

Seniors don’t just write code. They reduce uncertainty for others.

One‑pager RFCs beat long meetings

Before starting a feature, I send a 1‑page proposal with problem, scope, risks, and timeline. Feedback arrives async, decisions are documented, and I avoid rework.

# RFC: Feature Flags for Checkout
- Problem: Risky releases cause rollbacks
- Proposal: Introduce gradual rollout via server‑side flags (ConfigCat)
- Scope: Backend gating + metrics + owner
- Risks: Flag drift, stale code paths
- Timeline: 1 week

Practical tip

  • Prefer concise docs and diagrams. Link to tickets and dashboards. Keep history in PRs or RFCs.

3) Code quality is about readers, not authors

Readable code survives org changes. I learned to optimize for the next person (often future‑me).

Small modules, clear boundaries

I adopted a layered approach: route → handler → service → repository. Each layer has a single reason to change.

// handler.ts
export async function createUserHandler(req: Request, res: Response) {
  const input = parse(req.body, createUserSchema); // validation
  const user = await userService.create(input);    // orchestration
  res.status(201).json(user);
}

// user.service.ts
export async function create(input: CreateUserInput) {
  if (await userRepo.exists(input.email)) throw new Error('E_EXIST');
  const hash = await hashPassword(input.password);
  return userRepo.insert({ ...input, password: hash });
}

Guardrails > heroics

ESLint, Prettier, and strict TypeScript prevent entire classes of issues.

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true
  }
}

Practical tip

  • Add a pre‑push hook that type‑checks, lints, and runs fast tests. Catch problems before CI does.

4) Tests that matter

At first I wrote tests to raise coverage. Seniors write tests to prevent regressions and enable refactors.

Focus on behavior and seams

I aim for a triangle of tests: a few end‑to‑end, more service‑level, and unit tests for tricky logic.

// service test (fast, meaningful)
describe('priceService.calculate', () => {
  it('applies tiered discount and tax', () => {
    const price = priceService.calculate({ base: 100, tier: 'pro', region: 'EU' });
    expect(price.total).toBeCloseTo(108.5, 1);
  });
});

Make tests an API for future you

Write tests that read like documentation. Use factories over fixtures, and avoid mocking internals that will change.

Practical tip

  • When a bug appears, first write a red test reproducing it. Then fix the code. This guarantees it stays fixed.

5) Own the lifecycle: from design to on‑call

A senior mindset includes operability.

Observability from day one

Add structured logs and correlation IDs. Emit counters for success and errors. Create an on‑call runbook.

import pino from 'pino';
export const log = pino();

app.use((req, _res, next) => {
  (req as any).cid = crypto.randomUUID();
  log.info({ cid: (req as any).cid, path: req.path, method: req.method }, 'request');
  next();
});

Sharp edges and guardrails

  • Timeouts and retries for outbound calls
  • Circuit breakers for flaky dependencies
  • Feature flags and staged rollouts

Practical tip

  • Add a “production readiness” checklist to PRs: logging, metrics, dashboards, alerts, rollback plan.

6) Multiplying value through mentorship

Helping others is not charity—it’s leverage. Pairing, reviewing PRs with empathy, and sharing learning notes elevated the whole team.

Actionable code reviews

I switched from “what’s wrong?” to “how can this be clearer?”

Instead of: "Rename this."
Try: "This function does two things (parsing and persistence). Consider splitting to improve testability."

Practical tip

  • Schedule regular pairing. Keep a snippets folder for patterns you recommend often.

7) Manage your energy like a system constraint

Seniors are consistent, not just brilliant on good days. I learned to guard deep‑work blocks and automate toil.

Personal tooling that compounds

A few scripts saved me hours and reduced mistakes.

# scripts/redeploy.sh
set -euo pipefail
BRANCH=$(git rev-parse --abbrev-ref HEAD)

npm run test:ci
docker build -t app:$BRANCH .
docker push registry.example.com/app:$BRANCH
kubectl set image deploy/app app=registry.example.com/app:$BRANCH

Practical tip

  • Maintain a personal “ops” README with common commands, env details, and rollback steps.

Practical takeaways

  • Tie work to metrics. Outcomes beat output every time.
  • Communicate through short RFCs and clear PRs.
  • Design for readers: small modules, explicit boundaries, strict typing.
  • Test for behavior and refactor‑safety, not just coverage.
  • Build in observability, flags, and rollout plans from day one.
  • Multiply impact via mentorship and empathetic reviews.
  • Protect deep work and automate repetitive tasks.

Conclusion

Becoming “senior” is not a title you receive after enough sprints. It is the habit of creating clarity, delivering measurable outcomes, and making those around you more effective. The techniques above—impact focus, precise communication, reader‑friendly code, meaningful tests, operational ownership, and mentorship—compound over time. Start small, measure results, and your growth will be undeniable.

Built with ❤️ by Abdulkarim Edres. All rights reserved.• © 2025