Skip to content
|By Muhammad Wilayat Shah|1 views • 1 unique IPs

7 Steps to Solve Any Programming Problem More Efficiently

A repeatable seven-step process for turning a vague ticket into working, tested code, shown on one real example: detecting overlapping room bookings in JavaScript and PostgreSQL.

7 Steps to Solve Any Programming Problem More Efficiently

Most programming problems do not feel hard because the code is complicated. They feel hard because you start typing before you know exactly what you are solving. You get halfway through, discover an edge case, rewrite half the function and lose an hour. The fix is not more talent. It is a repeatable process you can run every time, whether the problem is an interview puzzle, a Laravel report that times out or a React form that keeps showing stale data.

This guide walks through seven steps using one realistic example from start to finish: detecting overlapping room bookings in a property or ERP-style system. If you want the bigger-picture view of planning work before code, see How Senior Developers Break Down Complex Problems Before Writing Code. This article stays closer to the keyboard.

Step 1: Restate the Problem in Your Own Words

Before anything else, write one or two sentences describing the problem as if you were explaining it to a colleague. If you cannot, you do not understand it yet, and no amount of code will fix that.

Our ticket says: "Show a warning when bookings clash." Restated, that becomes: Given a list of bookings, each with a room, a start time and an end time, return every booking that overlaps an earlier booking in the same room, so the UI can highlight it.

Notice what the restatement forces you to decide:

  • Inputs: a list of bookings with id, roomId, start and end.
  • Output: the bookings that need attention, not every possible conflicting pair.
  • Definition of done: the warning list is correct and fast enough for a few thousand bookings.

If something is unclear here, ask now. A two-minute question to the product owner is cheaper than a day spent on the wrong output.

Step 2: Work Through Concrete Examples

Abstract descriptions hide edge cases. Concrete examples expose them. Write down a handful of small inputs and the output you expect, by hand, before writing code:

  • Booking 09:00 to 11:00 and 10:30 to 12:00 in Room A: the second one conflicts.
  • Booking 13:00 to 14:00 and 14:00 to 15:30 in Room A: no conflict. One ends exactly when the next starts.
  • Same times in different rooms: no conflict.
  • A long booking 08:30 to 12:30 with two short ones inside it: both short ones conflict.
  • An empty list: return an empty list, not an error.

The second example is the important one. It tells you that intervals are half-open: a booking includes its start time but not its end time. That single decision changes a <= into a <, and getting it wrong is exactly the kind of bug that slips into production. These examples also become your first test cases later.

Timeline of room bookings for two rooms showing which bookings overlap and that bookings touching at the edges do not conflict

Step 3: Break the Problem Into Smaller Pieces

Large problems become manageable when you split them into pieces you can solve and check one at a time. For our example:

  1. Decide whether two bookings overlap.
  2. Only compare bookings in the same room.
  3. Find all bookings that overlap something earlier.
  4. Make it fast enough for real data.

The first piece is tiny and easy to get right on its own. Two half-open intervals overlap when each one starts before the other ends:

JavaScript
function overlaps(a, b) {
  return a.roomId === b.roomId && a.start < b.end && b.start < a.end;
}

Once that function is trusted, the rest of the problem is about how you apply it.

Step 4: Solve the Simplest Version First

Do not reach for the clever solution straight away. Write the obvious one, even if it is slow. A working brute-force version gives you something correct to compare against, and sometimes it turns out to be fast enough.

JavaScript
function findConflictsNaive(bookings) {
  const conflicts = new Set();
  for (let i = 0; i < bookings.length; i++) {
    for (let j = i + 1; j < bookings.length; j++) {
      if (overlaps(bookings[i], bookings[j])) {
        conflicts.add(bookings[j].id);
      }
    }
  }
  return [...conflicts];
}

This compares every pair, so the work grows with the square of the number of bookings. For a hundred bookings that is fine. For tens of thousands in a busy ERP calendar, it starts to hurt. Now you know where the real constraint is, instead of guessing. (Note that the naive version flags whichever booking comes later in the array, so for a fair comparison, sort the input by start time first.)

Step 5: Plan the Solution in Plain Steps

Before writing the faster version, describe it in plain language or pseudocode. If the plan does not make sense in words, it will not make sense in code.

  1. Sort bookings by room, then by start time.
  2. Walk through the list, remembering the booking in the current room that ends latest.
  3. If the next booking starts before that latest end, it is a conflict.
  4. Update the "latest end" whenever a booking ends later, and reset it when the room changes.

Tracking the latest end, not just the previous booking's end, is what catches the case where a long booking contains several short ones. Your Step 2 examples should make you confident of that before you write a line.

Step 6: Build in Small Increments and Test as You Go

Turn the plan into code, then immediately run it against the examples from Step 2. Small increments mean that when something breaks, you know which change broke it.

JavaScript
function findConflicts(bookings) {
  const sorted = [...bookings].sort((a, b) =>
    a.roomId === b.roomId
      ? a.start - b.start
      : String(a.roomId).localeCompare(String(b.roomId))
  );

  const conflicts = [];
  let latest = null; // booking that ends latest in the current room

  for (const booking of sorted) {
    const sameRoom = latest && latest.roomId === booking.roomId;

    if (sameRoom && booking.start < latest.end) {
      conflicts.push(booking.id);
    }
    if (!sameRoom || booking.end > latest.end) {
      latest = booking;
    }
  }
  return conflicts;
}

Sorting dominates the cost, so this runs in roughly n log n time instead of n². Now lock in the behaviour with tests built from your examples:

JavaScript
import assert from 'node:assert';

const t = (h) => h * 60; // minutes since midnight
const bookings = [
  { id: 'B1', roomId: 'A', start: t(9),    end: t(11) },
  { id: 'B2', roomId: 'A', start: t(10.5), end: t(12) },
  { id: 'B3', roomId: 'A', start: t(13),   end: t(14) },
  { id: 'B4', roomId: 'A', start: t(14),   end: t(15.5) },
  { id: 'B5', roomId: 'B', start: t(8.5),  end: t(12.5) },
  { id: 'B6', roomId: 'B', start: t(9.5),  end: t(10.5) },
  { id: 'B7', roomId: 'B', start: t(11),   end: t(13) },
];

assert.deepStrictEqual(findConflicts(bookings).sort(), ['B2', 'B6', 'B7']);
assert.deepStrictEqual(findConflicts([]), []);

A useful extra check is to generate random bookings and assert that the fast version and the naive version agree. The slow solution from Step 4 has now become your safety net.

Step 7: Refine, Then Reflect

Working code is not the finish line. Spend a few minutes on two questions.

Can it be clearer?

Rename vague variables, remove leftover debugging, and add a short comment where the reasoning is not obvious, such as why intervals are half-open. The next person to read this, possibly you in six months, will thank you.

Is code even the right place for this?

Reflection sometimes reveals a better solution outside your function. If bookings live in PostgreSQL, the database can refuse overlaps outright with an exclusion constraint, so a clash can never be saved in the first place:

SQL
CREATE EXTENSION IF NOT EXISTS btree_gist;

ALTER TABLE bookings
  ADD CONSTRAINT no_overlapping_bookings
  EXCLUDE USING gist (
    room_id WITH =,
    tstzrange(starts_at, ends_at, '[)') WITH &&
  );

The '[)' bound is the same half-open rule you discovered in Step 2. You might still keep the JavaScript version for showing warnings on imported or legacy data, but the guarantee now lives where the data does.

Finally, note what you learned. Here, the lessons were "decide how boundaries behave early" and "keep the slow version as a test oracle." Those carry over to the next problem, which is the real point of having a process.

Putting the Seven Steps Together

Here is the whole process in one place:

  1. Restate the problem and define what done means.
  2. Work examples by hand, especially the awkward edges.
  3. Break it down into pieces you can check individually.
  4. Solve the simplest version first, even if it is slow.
  5. Plan in plain steps before writing the better version.
  6. Build in small increments and test against your examples.
  7. Refine and reflect, including whether code is the right layer.

It can feel slower at first, especially when you are under pressure to ship. In practice it usually saves time, because most of the hours lost on a problem come from solving the wrong thing or chasing a bug you could have predicted from a simple example. Try it on your next ticket, and when something still goes wrong after release, the same habits apply to finding the root cause of a production bug.