DEV Community

Cover image for The Orphaned ID Bug from My First Job (And What It Taught Me About Being a Junior Dev)
Bradley Matera
Bradley Matera

Posted on

The Orphaned ID Bug from My First Job (And What It Taught Me About Being a Junior Dev)

frustrated

About two weeks into my first web-dev job, I had a Trello card that said the LMS was sometimes leaving orphaned IDs behind when assigned trainings were finished or deleted.

The app was a corporate LMS for a global manufacturer that made machine protection, chip and coolant management systems, and facility safety products.

This was not a small internal team tool. It was the training system for the whole company, and they wanted every training assignment for anyone to go through this broken LMS.

The frontend was simple: jQuery dialogs, table rows, and AJAX calls.

The bug was not a dramatic React failure. It was a row-based edge case in a table view.

Sometimes the AJAX response would arrive and the data would be missing. Sometimes the table would render, but an empty row would appear where an assignment should have been.

That was enough for the card to hit the board and for me to start investigating.

bug report

Why the bug was more than a blank row

At first, it was easy to dismiss this as a rendering issue.

Maybe the list component was failing to fill the template. Maybe the CSS was swallowing the text. Maybe the client was accidentally rendering an empty object.

Then I opened the network tab.

The API was returning assignment objects. Some of them had training_id, some of them had completed_at, and some of them had fields set to null or undefined.

The client never filtered those out. It rendered every row it received.

That meant the bug was not just on the page. It was in the data the page was given.

empty table row

What I was told

When I asked my senior for more context, the answer was basically, “Figure it out without AI.”

That line was a punch in the face, because this was the same team that had told me they liked that I understood AI. The same team that had hired me after I talked about how I used free tools to speed up small fixes and keep my commits focused.

AI feedback

I was not being asked to build a feature. I was being asked to debug a failure in a system I did not own.

I was not given:

  • the schema for assigned_trainings
  • the intended lifecycle of a completed assignment
  • whether deletes were supposed to cascade
  • whether completed_trainings was supposed to be a source of truth

I was only given the symptom and an expectation.

That is a very common junior-dev situation: the problem is clear, but the contract is not.

It got worse after I was fired.

The official reason was that I used AI too much. I had spent one month on the job, made forty small commits, and the only real problem I had found was one that was creating blank rows in the UI.

The contradiction stung. They paid for the work, accepted the fixes, and then told me they did not want my process. It left me angry, confused, and more than a little ashamed.

That was the context I brought to the bug: a team that liked the results but disliked the tool, a junior dev who felt stuck between expectations and execution.

Following the data trail like a junior detective

Once I stopped looking at the UI and started looking at the database, the problem became more concrete.

The symptoms were:

  • blank rows appeared after delete or completion
  • the user record still existed
  • the training record still existed in some form
  • the list view did not know which rows were stale

In plain English: the UI was still receiving references to assignments that had lost their parent record.

That usually means one of two things:

  • the database is missing a foreign key constraint, or
  • code that cleans up stale assignments is not running consistently.

I began checking the tables in the order the app was likely using them:

  • assigned_trainings
  • completed_trainings
  • users
  • trainings

I was looking for rows that should have been gone but were still visible.

The SQL I wrote to prove the orphan

I did not have a polished schema diagram. I had a terminal and a query editor.

The first query I used was a simple orphan finder.

SELECT a.id,
       a.user_id,
       a.training_id,
       a.assigned_at,
       c.id AS completed_id
FROM assigned_trainings a
LEFT JOIN completed_trainings c
  ON a.user_id = c.user_id
  AND a.training_id = c.training_id
WHERE c.id IS NULL;
Enter fullscreen mode Exit fullscreen mode

That returned assignment rows that had no matching completion row.

Then I extended it to catch assignments that referenced missing training metadata.

SELECT a.id,
       a.user_id,
       a.training_id,
       t.title AS training_title
FROM assigned_trainings a
LEFT JOIN trainings t ON a.training_id = t.id
WHERE t.id IS NULL;
Enter fullscreen mode Exit fullscreen mode

Those queries were not meant to be elegant. They were meant to answer one question:

Is there data in the system that should not still be shown?

The answer was yes.

Why the SQL mattered more than the frontend

This bug was not a broken component. It was stale data flowing through the app.

The frontend code was basically doing this:

  • fetch assignment rows
  • render each row
  • assume each row was valid

It was not validating status, it was not filtering by deleted_at, and it was not checking whether the associated training still existed.

That made the UI brittle.

So I shifted the fix to the place where the contract belonged: the database.

debugging

The cleanup fix I shipped

I did not have enough confidence to rewrite the whole lifecycle.

What I had enough confidence to do was this:

  1. Identify stale assignment rows with SQL
  2. Delete them in a controlled cleanup operation
  3. Confirm the UI stopped showing blank rows

The frontend trigger was tiny:

$.post('/cleanup-orphans', {}, function(response) {
  console.log('deleted orphans', response.deletedCount);
});
Enter fullscreen mode Exit fullscreen mode

The backend controller looked like this in rough form:

app.post('/cleanup-orphans', async (req, res) => {
  const orphanIds = await db.query(`
    SELECT a.id
    FROM assigned_trainings a
    LEFT JOIN completed_trainings c
      ON a.user_id = c.user_id
      AND a.training_id = c.training_id
    WHERE c.id IS NULL
  `);

  const deleted = await db.query(
    'DELETE FROM assigned_trainings WHERE id = ANY($1)',
    [orphanIds.rows.map(row => row.id)]
  );

  res.json({ deletedCount: deleted.rowCount });
});
Enter fullscreen mode Exit fullscreen mode

I also added a small log statement so I could see the number of deleted rows in the server output.

This was not a permanent fix.

It was a controlled cleanup that removed the broken rows and let the UI go back to behaving.

What this taught me about junior work

There are two kinds of fixes in a codebase like that:

  • quick patches that make the symptoms disappear
  • deeper fixes that make the system enforce the invariant

A junior developer is often asked to deliver the first one while learning the second.

This bug mattered because it exposed a gap in the system’s contract.

The frontend expected valid assignment rows. The database did not guarantee them.

Until that was fixed, the app was fragile.

It also mattered because I was not just fixing a bug for a product. I was fixing a bug while trying to prove that I belonged in that role.

After the firing, the problem became more than technical. It became personal.

I was unemployed again. I had a recruiter apologizing on the phone. I had a wife and kids depending on me. I felt worthless for a day, then angry, then confused.

That is what made the lack of guidance especially painful. I was being measured on results, but I was not given the information that would have led to a better result.

What I wish someone had said

If my senior had said any of the following, the work would have been clearer:

  • “This looks like a data integrity problem, not a UI bug.”
  • “We need to know whether assignment rows should be deleted, archived, or left as history.”
  • “Check if the assigned_trainings table has a foreign key on user_id.”
  • “Find out whether completed_trainings is the source of truth.”

Instead I got, “Figure it out.”

That is a useful outcome sometimes, but it is not a substitute for explaining the problem domain.

What I learned

  1. Blank rows are usually a data problem, not a render problem.
    The UI can only show what it receives.

  2. A query is a debugger.
    SQL is a powerful way to prove whether the data actually exists.

  3. CRUD is not enough without constraints.
    Adding a row is easy. Making sure it is still valid later is the hard part.

  4. Ask for the contract.
    If you are told to fix something, ask what the data model is supposed to guarantee.

  5. Patches are not the same as system fixes.
    A cleanup endpoint can make the app behave, but the real fix is enforcing the invariant at the source.

If I did the bug again today

I would still start with the same questions:

  • What exactly should a completed assignment look like?
  • Which table is the source of truth for assignment status?
  • Should deletes remove the row, or should the row stay marked as completed?

I would also look for missing constraints and missing cleanup paths.

In a better design, the database can help.

For example:

ALTER TABLE assigned_trainings
ADD CONSTRAINT fk_user
FOREIGN KEY (user_id)
REFERENCES users(user_id)
ON DELETE CASCADE;
Enter fullscreen mode Exit fullscreen mode

That would cause the database to delete assignments automatically when the user is deleted.

But even that is not always the right answer.

If the business needs a record of completed training for audits, a history row is the right fix, not a cascade delete.

That is why the first job of a debugging session is always: ask what the data should mean.

Final takeaways

This story is not about one query or one jQuery call.

It is about the way junior developer work often happens:

  • you are handed a symptom
  • you are expected to turn it into a fix
  • you may not be told what the system is supposed to enforce

I fixed this bug with SQL, with a cleanup endpoint, and with enough caution to avoid deleting something live.

It was not the perfect design.

It was the right fix for the moment.

The bigger lesson was not the query itself. It was how much the work depended on context.

If I had been told whether this was supposed to be a one-time cleanup or a permanent contract, the answer could have looked very different.

The code I wrote was useful. The team knowledge I did not get was the thing that would have made it better.

If you are a junior dev, learn to ask for the data contract.

If you are a senior dev, help the junior dev understand whether the fix is temporary or permanent.

If you are a team, document your schema and your data assumptions before the blank rows appear.

That is what separates a band-aid from a reliable system.

This story is not just about a blank row in an LMS. It is about how much more fragile a junior engineer feels when the rules keep changing.

A team can survive one fired junior dev if the work is real and the feedback is honest. It becomes a different problem when the team refuses to say what the contract is.

Want other junior dev stories? Explore #junior-dev on DEV.

Top comments (0)