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
tasktable 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 extendtask. The email server gets flooded with 10,000 notifications in an hour. - The fix: Either move the Business Rule to the
incidenttable specifically, or add a condition:current.sys_class_name == 'incident'. - The .NET equivalent: It's like putting a
[NotifyOnUpdate]attribute on the baseTaskclass and being surprised when every derived type triggers it. In EF, yourDbContext.SaveChangesinterceptor 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
Discriminatorcolumn. - sys_id: The primary key. A 32-character GUID. Like
Guid.NewGuid()in C#. - Dot-walking:
caller_id.department.nametraverses relationships — like EF'sInclude().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.