Writing Code for Future You

The developer who inherits your code in two years might be you. How to write software that's maintainable, deletable, and kind to your future self.

The Code You Write Today #

Two years from now, you’ll open a file you wrote today. You won’t remember why you made certain decisions. You won’t remember what you were optimizing for. You won’t remember the context.

You’ll be your own archeologist, digging through code trying to understand what past-you was thinking.

How kind will you be to future-you?

The Maintainability Mindset #

Maintainable code isn’t about cleverness. It’s about obviousness.

When I write code, I ask:

  1. Will I understand this in six months?
  2. Can someone else modify this without breaking everything?
  3. Can I delete this without cascading failures?

If the answer to any of these is “no,” I refactor.

Write Code to Be Deleted #

The best code is code you can delete. Here’s what that means:

1. Isolate Dependencies #

Bad:

// God help you if you need to change payment providers
async function createOrder(items: Item[]) {
  const total = calculateTotal(items);
  const stripeCharge = await stripe.charges.create({...});
  const dbOrder = await db.orders.create({...});
  await emailService.send({...});
  return dbOrder;
}

Good:

async function createOrder(items: Item[], paymentProvider: PaymentProvider) {
  const total = calculateTotal(items);
  const charge = await paymentProvider.charge(total);
  const order = await saveOrder({ items, chargeId: charge.id });
  await notifyCustomer(order);
  return order;
}

When you need to switch from Stripe to PayPal, you delete one file. Not rewrite the entire order flow.

2. Separate Concerns #

Bad:

// This function does too much
async function handleUserRegistration(req: Request) {
  const valid = validateEmail(req.body.email);
  if (!valid) return { error: "Invalid email" };

  const user = await db.users.create({
    email: req.body.email,
    hashedPassword: await bcrypt.hash(req.body.password, 10),
  });

  await emailService.sendWelcome(user.email);
  await analytics.track("user_registered", { userId: user.id });

  return { success: true, userId: user.id };
}

Good:

async function handleUserRegistration(req: Request) {
  const input = validateRegistrationInput(req.body);
  const user = await createUser(input);
  await postRegistrationTasks(user);
  return { success: true, userId: user.id };
}

Each function does one thing. You can test them independently. You can replace them without side effects.

Name Things Obviously #

I used to be clever with names. Short, abstract, “elegant.”

Then I had to maintain that code.

Now I’m obvious:

  • getUserById not getUser
  • calculateTotalWithTax not getTotal
  • isEmailVerified not verified

Clarity over brevity. Always.

Comments Are Apologies #

I rarely write comments. When I do, it’s because the code failed to explain itself.

Bad comment:

// Get the user
const u = await db.users.findOne({ id });

No comment needed:

const user = await db.users.findById(id);

Good comment (rare, but useful):

// HACK: Retry three times because the third-party API is flaky
// TODO: Replace with circuit breaker pattern (ticket: ENG-1234)
const result = await retryThreeTimes(() => api.call());

Comments should explain why, not what.

Structure for Understanding #

Code structure should guide understanding. Here’s my hierarchy:

src/
├── features/          # Business logic organized by feature
│   ├── users/
│   │   ├── createUser.ts
│   │   ├── deleteUser.ts
│   │   └── types.ts
│   └── orders/
│       ├── createOrder.ts
│       └── types.ts
├── services/          # External integrations
│   ├── email/
│   ├── payments/
│   └── database/
└── shared/            # Utilities used everywhere
    ├── validation/
    └── errors/

When a new developer joins, they can navigate by feature, not by technical layer. Looking for order creation? Check features/orders/. Not controllers/orderController.ts vs services/orderService.ts vs repositories/orderRepository.ts.

Types Are Documentation #

TypeScript isn’t just type safety—it’s documentation that never goes out of date.

Bad:

function processOrder(data: any) {
  // Hope you know what data looks like!
}

Good:

interface OrderInput {
  items: Array<{ productId: string; quantity: number }>;
  shippingAddress: Address;
  paymentMethod: PaymentMethod;
}

function processOrder(input: OrderInput): Promise<Order> {
  // The types tell you exactly what this expects and returns
}

Types are contracts. They tell future-you what’s expected. They prevent stupid mistakes. They enable fearless refactoring.

Test the Important Paths #

I don’t aim for 100% coverage. I aim for confidence.

Test the paths that matter:

  • Happy path: Does it work when everything is correct?
  • Edge cases: What about empty arrays? Null values? Malformed input?
  • Error handling: What happens when the database is down?

Don’t test implementation details. Test behavior.

Write for Humans, Optimize for Machines #

Write code that humans can understand. Let the compiler optimize.

Bad:

// "Optimized" but unreadable
const t = i.r((a, v) => a + v.q * v.p, 0);

Good:

const totalPrice = items.reduce(
  (sum, item) => sum + item.quantity * item.price,
  0
);

The difference in performance? Negligible. The difference in maintainability? Enormous.

The Question I Ask Myself #

Before I commit code, I ask:

If I saw this code for the first time in two years, would I understand it?

If the answer is no, I refactor.

Real Example: Refactoring for Clarity #

Before (clever, concise, confusing):

export const p = (d: any) => {
  const r = d.m?.f((v) => v.t === "p");
  return r ? { id: r.id, amt: r.a, s: r.s } : null;
};

After (obvious, maintainable, kind):

export function extractPaymentDetails(data: OrderData): PaymentSummary | null {
  const paymentRecord = data.metadata?.find(
    (record) => record.type === "payment"
  );

  if (!paymentRecord) {
    return null;
  }

  return {
    id: paymentRecord.id,
    amount: paymentRecord.amount,
    status: paymentRecord.status,
  };
}

Future-you will thank you.

The Bottom Line #

You’re not writing code for the computer. You’re writing code for the next developer.

That next developer might be a colleague. Might be a stranger. Might be you, two years older, with no memory of why you wrote it this way.

Be kind to that person. Write code that’s obvious. Write code that’s deletable. Write code that’s maintainable.

Write code for future-you.


Further Reading:

Want to discuss this?

I write about systems thinking, accessibility, and building software that lasts. If this resonated with you, let's talk.