‹ All interview questions

Core Concepts

Explain ServiceNow's table hierarchy and inheritance model.

The question

Explain ServiceNow's table hierarchy and inheritance model. How does it compare to class inheritance in C# or table-per-hierarchy in Entity Framework? Give a real example of when understanding this saves you from a production mistake.

Why This Matters in an Interview

ServiceNow tables support single-table inheritance — one of the most important architectural concepts to grasp. If you've used Entity Framework's Table-Per-Hierarchy (TPH) mapping, you already understand the core concept. Every ServiceNow table can extend another, and child records physically live in the parent table's database.

The C# Inheritance Parallel

// C# Class Hierarchy (what you know)
public class Task { // Base class
  public string Number { get; set; }
  public string State { get; set; }
  public DateTime CreatedOn { get; set; }
}

public class Incident : Task { // Inherits from Task
  public string Category { get; set; }
  public int Priority { get; set; }
}

public class Change : Task { // Also inherits from Task
  public string RiskLevel { get; set; }
  public DateTime PlannedStart { get; set; }
}

// Entity Framework TPH: All stored in one 'Task' table
// with a 'Discriminator' column

// ServiceNow: Identical pattern!
// task (base table) — contains Number, State, Created
//   ├── incident (extends task) — adds Category, Priority
//   ├── change_request (extends task) — adds Risk, Planned Start
//   ├── problem (extends task)
//   └── sc_req_item (extends task directly, not sc_task)

Why This Matters in Production

// THE MISTAKE: Querying the parent table returns ALL child records
var gr = new GlideRecord('task');
gr.addQuery('assigned_to', userId);
gr.query();
// This returns incidents + changes + problems + catalog tasks
// + everything else that extends 'task'
// Could be 500,000+ records instead of the 50 you expected!

// THE FIX: Query the specific child table
var gr = new GlideRecord('incident'); // Only incidents
gr.addQuery('assigned_to', userId);
gr.query();
// Now you get only incidents assigned to this user

// OR: Filter by sys_class_name on the parent table
var gr = new GlideRecord('task');
gr.addQuery('assigned_to', userId);
gr.addQuery('sys_class_name', 'incident'); // Discriminator!
gr.query();

Real-World Production Mistake

  • The scenario: A developer writes a Business Rule on the task table that sends an email notification on every update. They test it with incidents — works fine. But in production, it fires on every change request, problem, catalog item, and HR case update too — because they all extend task. The email server gets flooded with 10,000 notifications in an hour.
  • The fix: Either move the Business Rule to the incident table specifically, or add a condition: current.sys_class_name == 'incident'.
  • The .NET equivalent: It's like putting a [NotifyOnUpdate] attribute on the base Task class and being surprised when every derived type triggers it. In EF, your DbContext.SaveChanges interceptor would fire for every entity type in the TPH hierarchy.

Key Fields to Know

  • sys_class_name: The discriminator column. Every record knows what 'type' it is. Like EF's Discriminator column.
  • sys_id: The primary key. A 32-character GUID. Like Guid.NewGuid() in C#.
  • Dot-walking: caller_id.department.name traverses relationships — like EF's Include().ThenInclude() for navigation properties, but resolved on-the-fly.
💡 .NET Developer Pro Tip

Always ask 'does this table extend another table?' before writing any script. Check the table's 'Extends table' field in the dictionary. A Business Rule on a parent table affects ALL children — just like a virtual method override in C# applies to every derived class that doesn't override it.