A health plan that manages a single formulary has a straightforward data problem. A rebate aggregator or PBM that manages 50, 100, or 500 formularies that share a common base but differ in specific ways has an architectural challenge that will either scale elegantly or collapse under its own weight, depending entirely on how the data model is designed.
The parent-child formulary pattern solves this by establishing inheritance relationships between formularies. A parent formulary defines the baseline: which drugs are covered, at what tier, with what utilization management. Child formularies inherit all of the parent's decisions and then apply targeted overrides for specific clients, regions, or benefit designs.
Why Flat Copies Fail
The naive approach is to copy the entire parent formulary for each client and then edit the copy. This works when you have five clients. At fifty clients, it becomes unmanageable. Here is why:
- Update propagation. When the P&T committee adds a new drug to the base formulary, that change must be applied to every client formulary. With flat copies, this is a manual process that touches every copy individually.
- Consistency verification. How do you know that Client A's formulary reflects the latest base decisions? You have to compare the entire formulary against the parent, which at 5,000+ NDCs is not feasible manually.
- Storage explosion. A parent formulary with 5,000 entries, copied 200 times, creates 1,000,000 rows where 990,000 are identical to the parent. This is not just wasteful; it creates performance issues in reporting and analysis queries.
The Inheritance Model
The parent-child pattern stores only the differences. The parent defines the complete base formulary. Each child stores only its overrides, along with a reference to its parent. Resolving a child formulary means starting with the parent and applying the child's overrides on top.
-- Parent formulary: the complete base
CREATE TABLE formulary_parent (
formulary_id VARCHAR(20) NOT NULL,
ndc VARCHAR(11) NOT NULL,
gpi VARCHAR(14),
tier SMALLINT NOT NULL,
pa_required BOOLEAN DEFAULT FALSE,
step_therapy BOOLEAN DEFAULT FALSE,
quantity_limit INTEGER,
effective_date DATE NOT NULL,
PRIMARY KEY (formulary_id, ndc, effective_date)
);
-- Child overrides: only the differences
CREATE TABLE formulary_child_override (
child_id VARCHAR(20) NOT NULL,
parent_id VARCHAR(20) NOT NULL,
ndc VARCHAR(11) NOT NULL,
override_type VARCHAR(20) NOT NULL, -- 'MODIFY', 'ADD', 'EXCLUDE'
tier SMALLINT,
pa_required BOOLEAN,
step_therapy BOOLEAN,
quantity_limit INTEGER,
effective_date DATE NOT NULL,
reason TEXT,
approved_by VARCHAR(100),
PRIMARY KEY (child_id, ndc, effective_date),
FOREIGN KEY (parent_id) REFERENCES formulary_parent(formulary_id)
);
Resolving a Child Formulary
To materialize the complete formulary for a specific child, you merge the parent with the child's overrides:
def resolve_formulary(parent_id, child_id, db, as_of_date):
"""Build the complete formulary for a child by applying overrides."""
query = """
SELECT
p.ndc,
p.gpi,
COALESCE(o.tier, p.tier) AS tier,
COALESCE(o.pa_required, p.pa_required) AS pa_required,
COALESCE(o.step_therapy, p.step_therapy) AS step_therapy,
COALESCE(o.quantity_limit, p.quantity_limit) AS quantity_limit,
CASE WHEN o.ndc IS NOT NULL THEN 'OVERRIDDEN'
ELSE 'INHERITED' END AS source,
o.reason AS override_reason
FROM formulary_parent p
LEFT JOIN formulary_child_override o
ON p.ndc = o.ndc
AND o.child_id = %s
AND o.effective_date <= %s
AND o.override_type = 'MODIFY'
WHERE p.formulary_id = %s
AND p.effective_date <= %s
AND p.ndc NOT IN (
SELECT ndc FROM formulary_child_override
WHERE child_id = %s AND override_type = 'EXCLUDE'
)
UNION ALL
SELECT
o.ndc, NULL AS gpi, o.tier, o.pa_required,
o.step_therapy, o.quantity_limit,
'CHILD_ONLY' AS source, o.reason
FROM formulary_child_override o
WHERE o.child_id = %s
AND o.override_type = 'ADD'
AND o.effective_date <= %s
"""
return db.execute(query, [
child_id, as_of_date, parent_id, as_of_date,
child_id, child_id, as_of_date
])
Multi-Level Hierarchies
Some organizations need more than two levels. A national PBM might have a master formulary that feeds into regional formularies, which feed into employer-specific formularies. The pattern extends naturally to multiple levels of inheritance, though we recommend limiting depth to three levels. Beyond that, debugging override conflicts becomes difficult.
A useful rule of thumb: if more than 15% of a child's entries are overrides rather than inherited, the child may warrant its own parent formulary rather than being modeled as a heavy override set. Too many overrides defeat the purpose of the inheritance model.
Change Impact Across the Hierarchy
One of the most valuable capabilities of the parent-child model is impact analysis. When the P&T committee proposes a change to the parent formulary, the system can instantly calculate how many child formularies are affected, which ones have existing overrides on the affected drug (and would therefore not be impacted), and the total member population that would see a change.
This transforms P&T committee meetings. Instead of reviewing a proposed change in isolation, the committee sees the full downstream impact across all client formularies before they make a decision. That is the kind of intelligence that justifies the architectural investment.
Output Generation
Client-specific output files are generated by resolving the child formulary and then formatting the result according to the client's required file format. Because the resolution is a database operation, generating output for 200 clients is a batch job that runs in minutes rather than a manual process that takes days.
The parent-child pattern is not new. Database inheritance models have existed for decades. What is new is applying this pattern to formulary management, where the scale of the problem (thousands of drugs, hundreds of clients, weekly updates) makes the architectural choice consequential. Get it right and the system scales linearly with client count. Get it wrong and operations scale linearly with analyst headcount.