Compliance Automation and Tooling Questions
Using technology to make compliance continuous rather than point-in-time. Covers GRC platforms and control libraries (build versus buy, one control set mapped to many regulations, requirement-to-evidence traceability, risk roll-ups), compliance-as-code and policy-as-code checks mapped to framework controls (including testing, versioning and retiring the rules, and turning local legal and privacy obligations such as retention and masking into machine-checkable tests), continuous control monitoring and drift alerts, automated evidence collection and tamper-evident evidence pipelines for auditors, automated compliance and privacy reporting with traceable lineage (including regulatory SLA tracking such as DSAR deadlines), and wiring compliance checks into engineering pipelines without slowing teams. Focuses on what to automate first, how to map controls to machine-checkable tests, where to place checks, and the limits of automation. Does not cover designing or testing the controls themselves, the content of individual frameworks, hands-on security mechanics such as secrets, IAM, scanning or logging pipelines, or conducting the audit.
Your company runs separate risk registers per product line and region, so leadership cannot see aggregate risk or compliance posture. How would you design the data model and ingestion for a GRC platform that rolls these up automatically, how would you keep scoring consistent across teams, and which executive views would you build from it?
Sample Answer
Direct answer
Build one canonical model (business unit, risk, control, link between them) with a single taxonomy (one shared list of risk categories) and a single anchored scoring rubric (a scoring guide that ties each level from 1 to 5 to concrete descriptions, so two people land on the same number), and ingest each local register through a mapping layer that normalizes into it. Roll up with worst-case and counts, never averages: nine risks scored 2 and one scored 15 average 3.3, which looks harmless, while the worst case (15) and the count of high risks (1) show the problem. Put three executive views on top. The hard part is not the tables. It is getting teams to score the same way and keeping the data fresh.
Data model (core record fields)
- business_unit: product line and region.
- risk: taxonomy category, owner, status (open, treating, accepted, closed), inherent likelihood and impact, residual likelihood and impact (inherent is before controls, residual after), last reviewed date, source system and source id.
- control and a risk_control link: last test result and date.
- history: a snapshot taken daily so trends exist.
Ingestion
- Extract from each register (spreadsheet, ticket tool, an existing GRC tool) on a schedule or via API.
- Map local categories to the shared taxonomy through a mapping table owned by risk management. An unmapped category is quarantined (held in a review queue) with an alert, not dropped.
- Validate: scores 1 to 5, owner present, review date present. Rejects go back to the register owner.
- Upsert (insert a new row, or update it if it already exists) keyed by source system plus source id, so reruns do not duplicate rows.
- Link duplicates: one vendor risk appearing in three products should point to a parent risk so the roll-up does not triple count it.
Consistent scoring
Use one 5x5 grid where score = likelihood x impact and high means 15 or above. Anchor each level with descriptors (for impact, ranges of financial loss, records exposed or downtime that leadership approved), run calibration sessions (meetings where teams score the same sample risks, compare, and discuss each difference until they converge), and have risk management re-score a small random sample each quarter. Show both the team's score and the re-scored one, so disagreement is visible rather than argued over. Roll-up uses ordinal scores (numbers that only rank risks, where the gaps between levels are not equal amounts), so do not sum them (ten minor risks scoring 2 each sum to 20, which would wrongly outrank one severe risk scoring 15), and do not mix them with quantified loss estimates from units that use FAIR (a method that estimates loss in money).
Worked roll-up
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE business_unit (id TEXT PRIMARY KEY, product_line TEXT, region TEXT);
CREATE TABLE risk (
id TEXT PRIMARY KEY, unit_id TEXT REFERENCES business_unit(id),
category TEXT, -- from the shared taxonomy
owner TEXT, status TEXT, -- open / treating / accepted / closed
likelihood INTEGER CHECK (likelihood BETWEEN 1 AND 5),
impact INTEGER CHECK (impact BETWEEN 1 AND 5),
residual_likelihood INTEGER CHECK (residual_likelihood BETWEEN 1 AND 5),
residual_impact INTEGER CHECK (residual_impact BETWEEN 1 AND 5),
last_reviewed TEXT
);
CREATE TABLE control (id TEXT PRIMARY KEY, last_test_result TEXT); -- pass / fail
CREATE TABLE risk_control (risk_id TEXT, control_id TEXT);
""")
db.executemany("INSERT INTO business_unit VALUES (?,?,?)", [
("BU1", "payments", "EU"), ("BU2", "payments", "US"), ("BU3", "lending", "EU")])
db.executemany("INSERT INTO risk VALUES (?,?,?,?,?,?,?,?,?,?)", [
("R1", "BU1", "third_party", "ana", "open", 4, 5, 3, 5, "2026-09-20"),
("R2", "BU2", "third_party", "bo", "treating", 3, 4, 2, 4, "2026-06-01"),
("R3", "BU3", "data_privacy", "cy", "open", 5, 3, 3, 3, "2026-09-28"),
("R4", "BU1", "access", "ana", "accepted", 2, 5, 2, 5, "2026-09-01"),
])
db.executemany("INSERT INTO control VALUES (?,?)", [("C1","pass"),("C2","fail")])
db.executemany("INSERT INTO risk_control VALUES (?,?)", [("R1","C1"),("R2","C2"),("R3","C1")])
print("Residual score = residual_likelihood x residual_impact; HIGH means score >= 15")
q = """
SELECT b.product_line, b.region, r.category,
COUNT(*) AS risks,
MAX(r.residual_likelihood * r.residual_impact) AS worst_residual,
SUM(r.residual_likelihood * r.residual_impact >= 15) AS high_count,
SUM(julianday('2026-10-04') - julianday(r.last_reviewed) > 90) AS stale_reviews
FROM risk r JOIN business_unit b ON b.id = r.unit_id
WHERE r.status <> 'closed'
GROUP BY b.product_line, b.region, r.category
ORDER BY worst_residual DESC, b.product_line, b.region
"""
for row in db.execute(q):
print(row)
print("Open risks with no linked control, or only failing controls:")
q2 = """
SELECT r.id, r.status FROM risk r
WHERE r.status <> 'closed' AND NOT EXISTS (
SELECT 1 FROM risk_control rc JOIN control c ON c.id = rc.control_id
WHERE rc.risk_id = r.id AND c.last_test_result = 'pass')
ORDER BY r.id
"""
for row in db.execute(q2):
print(row)
Residual score = residual_likelihood x residual_impact; HIGH means score >= 15
('payments', 'EU', 'third_party', 1, 15, 1, 0)
('payments', 'EU', 'access', 1, 10, 0, 0)
('lending', 'EU', 'data_privacy', 1, 9, 0, 0)
('payments', 'US', 'third_party', 1, 8, 0, 1)
Open risks with no linked control, or only failing controls:
('R2', 'treating')
('R4', 'accepted')
How the SQL works: the comparison inside SUM(...) is true (counted as 1) or false (counted as 0) for each row, so summing it counts how many rows meet the condition, here scores of 15 or more or reviews older than 90 days. julianday turns a date into a day number so that subtracting two dates gives the number of days between them. GROUP BY produces one output row per product line, region and category, with MAX giving the worst score in each group. The second query lists open risks for which no linked control has a passing test.
Reading the output: payments in the EU has one high residual risk (R1 scores 3 x 5 = 15, inherent 4 x 5 = 20). R2 is the only stale one: reviewed on 2026-06-01, 125 days before 2026-10-04, against a 90-day rule. R2 and R4 have no passing control: R2's only control failed its test, and R4 is accepted with no control, so its acceptance record is what the platform should surface.
Executive views
- Residual heat map (a grid colored by likelihood and impact) by product line and region, showing count of high risks and the worst score (not the mean).
- Top risks above appetite (risk appetite is the level of risk leadership agrees to accept; here, a residual score of 15 or more is above it), with owner, age, treatment status and next date.
- Control health and data quality: failed controls, risks with no passing control, share of risks with an owner, share reviewed in the last 90 days.
Limits
A rolled-up number is only as honest as its inputs. A register with stale reviews will look calm, which is why staleness is a first-class metric. Automatic aggregation cannot tell whether a team under-scored, which is what calibration is for.
Auditors reviewing your SOX and GDPR compliance reports keep asking 'where did this number come from, and can you reproduce it for last quarter?'. How would you design the compliance reporting layer so every metric has traceable lineage, reports regenerate identically for past periods, access is controlled, and a multi-region company can still deliver them within a few hours?
Sample Answer
Direct answer
Treat every reported number as the output of a versioned, re-runnable pipeline over frozen inputs, never as something someone queried and pasted. Four things make auditors stop asking "where did this come from": frozen source snapshots, metric definitions kept as versioned code, lineage metadata written automatically with every run, and an as-of rerun that produces the same bytes. Access control and regional delivery sit on top of that foundation.
Key terms
- SOX (Sarbanes-Oxley Act, US law): section 404 requires management to assess the effectiveness of internal control over financial reporting, and for larger filers an external auditor attests to that assessment. For engineers this means the ITGCs (IT general controls: access, change management, operations) behind financial systems, plus any metric that evidences them.
- GDPR (EU General Data Protection Regulation): compliance metrics here are things like DSAR counts (data subject access requests) and breach-clock timings.
- Lineage: a recorded chain from a reported number back to the source rows and the code version that produced it.
The design, layer by layer
- Frozen inputs. At each period close, extract source data into an immutable, dated snapshot (write-once storage, meaning a location where saved files cannot be edited or deleted for a set period, with a content hash stored beside it: a content hash is a short fingerprint computed from the file's bytes, and changing even one byte produces a completely different fingerprint, so a matching fingerprint shows the bytes are identical). Later corrections to the live system do not rewrite history; they arrive as a new, labelled restatement, meaning a clearly marked later version of the figures that sits next to the original instead of replacing it.
- Metric definitions as code. Each metric (for example
mfa_coverage_v3) lives in version control with an owner, a plain-English definition and tests. A definition change gets a new version, so last quarter's report is regenerated with last quarter's definition. - Lineage captured by the pipeline. Each run writes a manifest: metric version, code commit, input snapshot hash, row count, run time, who approved. Nobody fills this in by hand.
- As-of regeneration. Rerunning with the same snapshot and the same code must give identical output, and the pipeline compares the new hash with the archived one. A mismatch is an alert, not a surprise during the audit.
- Access control. Roles separate who can read published reports (auditors, finance, compliance: read-only), who can change metric definitions (through reviewed pull requests only), and who can approve a release. Nobody edits a published output; corrections go through a new run. Access to the underlying personal data is narrower than access to the aggregates, and every read of the report store is logged.
- Multi-region within hours. Run the metric computation inside each region on that region's snapshot, publish only aggregates (counts, percentages) to a central report store, and schedule regional jobs to finish before a fixed global cut-off. If personal data cannot leave a region, only the aggregate does. A concrete case using the five users in the worked example below: each region sends its count of users with MFA and its count of users, not a percentage. EU sends 1 of 2, US sends 2 of 2, APAC sends 0 of 1. The central store adds the counts to get 3 of 5 = 60.0%. Averaging the three regional percentages (50.0, 100.0, 0.0) would give 50.0%, which is wrong, so regions send numerators and denominators. Counsel decides what counts as transferable; engineering enforces it with region-pinned compute (the job runs on servers inside the region, so the personal data never leaves it). A completeness gate (a check that blocks publication until every expected input has arrived) holds the consolidated report until every region has reported, and shows which one is late instead of silently omitting it.
Worked example (code run, output pasted)
import hashlib, json
snapshot = [
{"region": "EU", "user_id": 1, "mfa": True},
{"region": "EU", "user_id": 2, "mfa": False},
{"region": "US", "user_id": 3, "mfa": True},
{"region": "US", "user_id": 4, "mfa": True},
{"region": "APAC", "user_id": 5, "mfa": False},
]
METRIC_VERSION = "mfa_coverage_v3"
def mfa_coverage(rows):
return round(100 * sum(r["mfa"] for r in rows) / len(rows), 1)
def digest(obj):
return hashlib.sha256(json.dumps(obj, sort_keys=True).encode()).hexdigest()[:12]
def run(rows):
return {"metric": METRIC_VERSION, "value": mfa_coverage(rows),
"input_sha": digest(rows), "n_rows": len(rows)}
first = run(snapshot)
rerun = run(list(snapshot))
print(first)
print("regenerates identically:", first == rerun)
Output:
{'metric': 'mfa_coverage_v3', 'value': 60.0, 'input_sha': '4fa2459b72e3', 'n_rows': 5}
regenerates identically: True
Reading the code: mfa_coverage is the metric, the share of rows where mfa is true. digest turns the rows into text with json.dumps(..., sort_keys=True) (sorting the keys means the same data always produces the same text, whatever order the fields were built in), feeds that text to SHA-256 (a standard fingerprinting function), and keeps the first 12 characters, enough to compare by eye. run returns the metric version, the value, the input fingerprint and the row count, which is a small lineage manifest. The last lines run the metric twice on the same data and show the two results are equal. If a single row had changed between runs, input_sha would differ, and that is how a mismatch becomes visible.
Three of five users have MFA, so 60.0. The auditor's question "can you reproduce it for last quarter" is answered by rerunning against the archived snapshot and showing the identical hash.
Trade-offs and pitfalls
- Snapshots cost storage; keep full snapshots only for in-scope metrics and the retention period your auditor and counsel set.
- Non-deterministic queries (unordered results, "now()" in SQL, floating-point sums across partitions) break identical regeneration. Pin the as-of timestamp and the ordering.
- Dashboards pointing at live tables are not evidence. Publish a dated, hashed export for the audit trail.
- A fast regional merge is worthless if one region silently drops out, so fail visibly on missing inputs.
A global enterprise wants one control library that maps to many regulations and frameworks, and regulations keep changing. How would you decide between buying a GRC platform and building your own, how would you structure the crosswalk so one change updates everything affected, and which systems would you integrate first?
Sample Answer
Direct answer
I would buy a GRC platform for the workflow, evidence collection and auditor-facing views, and build a thin layer of my own for what must stay portable: the unified control library and the crosswalk, stored as structured data I own and can export. Buy the commodity, own the model. That avoids a year of building ticket-and-workflow software while avoiding lock-in on the part that encodes the company's own control decisions.
Key terms
- GRC (governance, risk and compliance): software that holds controls, risks, policies, evidence and audit workflows.
- Crosswalk: a mapping that links one control to every framework requirement it satisfies.
- Unified control library: one set of company controls, each with one owner and one test, mapped to many frameworks, instead of separate control sets per framework.
- SOC 2: an auditor's report on whether a company's security controls work, issued under the American Institute of CPAs (AICPA) criteria. ISO 27001: the international standard for running an information security management system. NIST SP 800-53: a US government catalog of security controls, where IA-2(1) is the entry requiring multi-factor authentication (a second proof of identity beyond a password) for privileged accounts.
- Lock-in: being unable to leave a vendor without losing, or having to rebuild, your own data and decisions.
Buy versus build
| Factor | Leans buy | Leans build |
|---|---|---|
| Frameworks | Several common published ones (the SOC 2, ISO 27001 and NIST frameworks above) | Unusual or internal-only |
| Team | Small compliance team, few engineers to spare | Strong platform team, existing data stack |
| Integrations | Vendor has connectors for your systems | Mostly homegrown systems |
| Time to first audit | Soon | Flexible |
| Cost | License plus administration | Engineers plus maintenance forever |
| Lock-in | Can export controls and mappings | Full control |
Illustrative cost, same annual basis: buy is a 120k license plus half an administrator at 150k loaded (salary plus benefits and overhead), which is 75k, 195k per year; build is 1.5 engineers at 150k, 225k per year, before the cost of maintenance and not shipping product work. The numbers are invented for the comparison; the point is that costs are close, so capability and time decide. I would flip to build if the estate is mostly custom systems the vendor cannot connect to, or no vendor can model the company's frameworks.
Crosswalk structure so one change updates everything
Keep four linked layers, each versioned:
- Framework requirement: framework, version, clause id, text.
- Mapping (many to many: one requirement can map to several controls, and one control to several requirements): requirement to unified control, with strength (full or partial), reviewer and date.
- Unified control: statement, owner, test, systems in scope.
- Test and evidence: result, timestamp, artifact.
Filled in for one control (illustrative values), the four layers look like this:
| Layer | Row |
|---|---|
| Requirement | NIST SP 800-53 r5, IA-2(1), "multi-factor authentication for access to privileged accounts" |
| Requirement | ISO/IEC 27001:2022, Annex A 8.5, secure authentication |
| Requirement | SOC 2, CC6.1, logical access security |
| Mapping | IA-2(1) to CTL-MFA-ADMIN, full, reviewer j.doe, 2026-09-12 |
| Mapping | A.8.5 to CTL-MFA-ADMIN, full, reviewer j.doe, 2026-09-12 |
| Mapping | CC6.1 to CTL-MFA-ADMIN, partial (CC6.1 also needs other controls), reviewer j.doe, 2026-09-12 |
| Control | CTL-MFA-ADMIN, "All administrator accounts require MFA", owner iam-team, test admin_mfa_enforced, systems idp-prod |
| Test and evidence | 2026-10-03, pass, 0 of 41 admins without MFA, artifact a1f09c |
If a framework later renumbers A.8.5, only that requirement row and its mapping row change. The control, its test and the evidence history stay untouched.
Because requirements point at controls rather than at each other, changing one control's test updates every framework it supports. A new framework revision adds new requirement rows, and the diff immediately lists controls with unmapped new requirements. Where available, export in an open format; NIST's OSCAL (Open Security Controls Assessment Language) offers machine-readable catalogs, baselines and assessment data in XML, JSON and YAML.
Using automation to suggest mappings
A language model or text-similarity tool can propose candidate mappings. Store them as suggested, never approved. A person reviews each one; ambiguous requirements are marked partial and escalated to the control owner, counsel or the auditor, who decides what counts. Track the acceptance rate (the share of suggested mappings that reviewers approve), since a low rate means the suggestions are noise.
Which systems to integrate first
- Identity provider: access, MFA and joiner-leaver evidence supports many controls.
- Cloud account and asset inventory: defines scope and supplies configuration checks.
- Ticketing and change management: remediation and approval evidence.
- SIEM (security event log platform): logging and detection evidence, after the basics.
The reason is coverage per integration: identity and inventory feed the most controls for the least effort.
Worked example
Unified control "admins use multi-factor authentication" maps to NIST SP 800-53 IA-2(1) and to the matching SOC 2 (CC6.1) and ISO 27001 (Annex A 8.5) requirements, as the three mapping rows above show. One check against the identity provider updates all three.
Pitfall
Starting with per-framework control lists inside the tool, which recreates the duplication the unified library exists to remove.
The monthly compliance report is assembled by hand from several systems and is always late and occasionally wrong. How would you automate it end to end, what would you build in so a bad upstream feed does not silently produce a confident-looking report, and who would you let see and edit it?
Sample Answer
Direct answer
Replace the hand assembly with a scheduled pipeline that pulls each source through a data contract (a written agreement between the team that owns a feed and the team that consumes it: which fields, what format, how fresh, delivered when), runs data-quality gates, computes metrics from versioned definitions and publishes a dated report. The key design choice is that the pipeline fails closed, meaning that when it cannot be sure the data is good it refuses to present a final result instead of carrying on: when a feed is stale, empty or malformed, the report is marked incomplete and says why, rather than printing a tidy number built on bad data.
Step 1: Inventory the manual process
List every source system, who exports it, which spreadsheet formulas touch it and which numbers have been wrong before. Those past errors become the first automated tests.
Step 2: Build the pipeline
- Ingest from each system by API or scheduled export into a staging area, keeping the raw copy untouched.
- Validate each feed before use (see the next section).
- Transform with metric definitions in version control, each with an owner and a written definition.
- Publish a dated report plus a manifest (inputs used, row counts, definition versions, run time), on a schedule a day or two before the deadline so a failure can be fixed before it is late.
Step 3: Guards so a bad feed cannot hide
| Guard | What it catches |
|---|---|
| Freshness check | Feed last updated before the expected window |
| Volume check | Row count far from the recent norm (a feed that normally has 1,200 rows and arrives with 0 trips a 50% floor of 600 rows) |
| Schema check | Renamed or missing columns, changed types |
| Reconciliation | Total in the report disagrees with an independent control total (a headline figure the source system reports on its own, such as "1,213 active employees", which the report's own count must also match) |
| Per-section status | Each section shows complete, partial or missing, so one bad feed taints only its own section |
If any blocking guard fails, the report is stamped DRAFT: INCOMPLETE with the failing feed named, the owner is alerted, and nobody can publish it as final without a recorded override. The override requires a reason and a named approver.
Step 4: Who sees and edits
- View: audience-based roles (compliance team, executives, auditors), read-only. Sensitive detail (individual names, incident narratives) is restricted to people who need it.
- Edit: nobody edits the output. People change inputs at the source or change definitions through a reviewed change. The only manual path is a logged override with a reason.
- Approve: the report owner signs off on each period; that approval is stored with the manifest.
Worked example
Six feeds feed the monthly report. On the 3rd, the HR export arrives with 0 rows. The volume check (floor 600, because the usual is 1,200) fails. The old process would have shown "0 leavers without access revoked", which looks excellent and is wrong. The new pipeline marks the joiner-mover-leaver section (the part covering people who join the company, change roles or leave, and whether their access was granted, changed or removed on time) missing, sends the HR-data owner an alert, and the report goes out on schedule, stamped DRAFT: INCOMPLETE, with five sections complete and one flagged missing; it can be released as final only through the recorded override.
Trade-offs and pitfalls
- Over-tight thresholds create alert fatigue (so many alerts that people stop reading them); seed them from recent history and review them quarterly.
- Automation cannot tell that a definition is wrong, only that it is applied consistently. Keep a human review of definition changes.
- Keep the old manual report in parallel for one or two cycles and diff them, which finds the hand-assembly errors.
Your privacy team misses an occasional data subject access request deadline because nobody notices a backlog building until it is too late. How would you build the tracking and alerting so at-risk requests surface early without paging people for every open ticket, and how would you test that it works before the next backlog spike?
Sample Answer
Direct answer
A data subject access request (DSAR) is a person's request to see the personal data an organization holds about them. The legal clock starts at receipt. Under GDPR Article 12(3) the response is due without undue delay and within one month of receipt, extendable by two further months where necessary given the complexity and number of requests, with the data subject told of the extension and reasons within the first month. California's CCPA (California Consumer Privacy Act, Civil Code 1798.130) gives 45 days, extendable once by 45 days with notice. Which one applies depends on who the requester is and which law covers your organization for them. Counsel owns that mapping, and where two could apply the tracker uses the earlier date. For example, a request received on 3 March 2025 is due 3 April 2025 under GDPR (one month) and 17 April 2025 under CCPA (45 days), and the tracker stores that computed date rather than leaving someone to count. I would track every request with computed due dates, project each one's completion from the queue and team capacity, and flag only requests projected to miss. Counsel decides when an extension is justified and whether any pause applies.
Data model
One record per request: received_at, jurisdiction, due_at computed by rule (kept in one table counsel owns), current stage (identity check, locating data, collection, legal review, delivery), stage timestamps, owner, and extension status. Timestamps per stage give you throughput, which is what makes projection possible.
Alert tiers instead of one page per ticket
- Daily digest: requests whose projected completion date is later than due date minus a safety buffer.
- Page: a flagged request with no owner action for a stated time, or a queue projected to breach on several requests.
- Weekly capacity view: arrivals versus completions and backlog trend, so a spike is seen as a rate problem before individual requests are at risk.
- Heartbeat: an alert if the tracker itself has produced no data, because a silent alerting system looks identical to a calm one.
Testing it before the next spike: a runnable simulation
The core alert rule is simple. A request's position in the queue divided by the team's daily capacity is the number of working days before the team reaches it. Add the safety buffer, and if that total is more than the days left until its due date, flag it now, while there is still time to add people. For example, with 40 requests ahead of it and a capacity of 4 a day, a request needs about 10 days of work before it is handled. With a 3-day buffer that is 13 days. If it has 12 days left it is flagged. If it has 13 or more it is not yet. In the code below that rule is the line if pos / BASE_CAP + BUFFER > days_left, where pos is how many requests are ahead of this one and days_left is its arrival day plus 30 minus today.
This toy model (illustrative parameters, not your data) uses Poisson arrivals (random counts around an average) with a fixed seed. Normal days average 3 requests, and a ten-day spike runs at 20 a day. The team completes 4 a day, so on a normal day it has only 1 spare slot, and it drains a spike backlog by only about 1 request a day. The spike brings about 200 arrivals in ten days against 40 completions, so the queue grows by roughly 160. In the run below, with no early warning, the queue peaks at 154 waiting requests on day 69 and is not fully cleared until about day 199. A 30-day internal target stands in for the legal deadline, the safety buffer is 3 days, and the response to an early flag is to double capacity to 8 a day.
import math, random
DAYS, DUE, BASE_CAP, SURGE_CAP, BUFFER = 200, 30, 4, 8, 3
def poisson(rng, lam):
L, k, p = math.exp(-lam), 0, 1.0
while True:
p *= rng.random()
if p <= L:
return k
k += 1
def run(surge_on):
rng = random.Random(7)
queue, breaches, rid = [], 0, 0 # queue = [(id, arrival_day)], oldest first
naive_pages, flagged, surge_days = 0, set(), 0
for day in range(DAYS):
lam = 20 if 60 <= day < 70 else 3 # a ten-day backlog spike
for _ in range(poisson(rng, lam)):
queue.append((rid, day)); rid += 1
cap = BASE_CAP
newly = 0
for pos, (r, arr) in enumerate(queue):
naive_pages += 1 # what "page on every open ticket" would send today
days_left = arr + DUE - day
if pos / BASE_CAP + BUFFER > days_left and r not in flagged:
flagged.add(r); newly += 1
if surge_on and any(r in flagged for r, _ in queue):
cap, surge_days = SURGE_CAP, surge_days + 1
for _ in range(min(cap, len(queue))):
r, arr = queue.pop(0)
if day - arr > DUE:
breaches += 1
return rid, breaches, naive_pages, len(flagged), surge_days, len(queue)
for label, s in (("no early warning", False), ("early warning + surge", True)):
rid, b, naive, fl, sd, left = run(s)
print(f"{label}: received={rid} breaches={b} flagged={fl} surge_days={sd} still_open={left} naive_pages={naive}")
Real output:
no early warning: received=710 breaches=107 flagged=167 surge_days=0 still_open=0 naive_pages=11377
early warning + surge: received=710 breaches=0 flagged=70 surge_days=23 still_open=0 naive_pages=3715
Without early warning, 107 requests finish after day 30. With the flag and the surge response, 0 do. The variable naive_pages counts one page per open request per day, so a request that stays open for 20 days adds 20 pages. A rule that paged on every open ticket would therefore have sent 3,715 pages across the run in the second case, against 70 distinct requests actually flagged, each flagged once. The two numbers count different things (request-days versus unique requests), which is the point: the projection rule turns thousands of daily reminders into a short list of requests that need action. The model shows the rule would have worked on that spike, not that it works on yours.
Testing on your own data
- Backtest: replay last year's queue history through the rule. List which past misses it would have flagged and how many pages per week it would have sent.
- Synthetic requests: inject back-dated dummy requests in staging and confirm they appear in the digest and escalate on schedule.
- Game day: simulate a spike with the real team and check the escalation path, not just the dashboard.
- Track alert quality: precision (flagged requests that truly needed action) and lead time (days left when first flagged).
Trade-offs
A bigger buffer catches more but pages more. Start with a buffer from your stage-duration history, review precision monthly, and tune. Be careful treating the internal target and the legal deadline as the same number: keep your target earlier than the strictest deadline that applies.
Unlock Full Question Bank
Get access to all 7 Compliance Automation and Tooling interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.