HR Architect Coding Problems
Ten problems tailored to this role. Date math, identity dedup, idempotency under retry, EIB-shaped transforms — the patterns that show up in production HR work.
How to approach each problem out loud
- Restate: "Just to confirm — you want me to..."
- Constraints: input size, duplicates, missing fields, time-zone, effective dates?
- Example: walk a small case
- Brute force: mention it
- Pattern: hash map? sort + sweep? two-pointer? date arithmetic?
- Complexity: state it
- Code with narration
- Test: walk through your example
- Tradeoffs: edges, what you'd add for production
1Deduplicate workers across systems
IdentityHash map
Prompt: You're given lists of workers from Workday, Ashby, and Okta. Each has name and email. Some workers appear in multiple systems. Some have multiple emails (work, personal). Some emails are typo'd. Produce a single list with one row per logical worker. Decide on a deterministic tiebreak.
Show solution
Approach
Normalize emails (lowercase, strip), build a union-find by email. For typos, accept that perfect identity dedup needs a stronger key (govt ID hash). Document the limit.
from collections import defaultdict
class UnionFind:
def __init__(self):
self.parent = {}
def find(self, x):
self.parent.setdefault(x, x)
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x
def union(self, a, b):
ra, rb = self.find(a), self.find(b)
if ra != rb:
self.parent[ra] = rb
def dedup_workers(*sources: list[dict]) -> list[dict]:
uf = UnionFind()
record_by_email: dict[str, list[dict]] = defaultdict(list)
# Step 1: normalize and index
for source in sources:
for w in source:
for raw_email in (w.get("emails") or [w.get("email")]):
if not raw_email:
continue
e = raw_email.strip().lower()
record_by_email[e].append(w)
uf.union(e, e) # ensures presence
# Step 2: connect any two emails that appear on the same record
for source in sources:
for w in source:
emails = [e.strip().lower() for e in (w.get("emails") or [w["email"]]) if e]
for e in emails[1:]:
uf.union(emails[0], e)
# Step 3: group by root
groups: dict[str, list[dict]] = defaultdict(list)
for email, records in record_by_email.items():
groups[uf.find(email)].extend(records)
# Step 4: collapse each group deterministically (prefer Workday source)
result = []
for root, records in groups.items():
records.sort(key=lambda r: (r.get("source") != "workday", r.get("worker_id") or ""))
canonical = records[0]
canonical["aliases"] = sorted({r.get("source") for r in records})
result.append(canonical)
return result
Drill notes
- Complexity: O(N·α(N)) for the union-find passes.
- Edge: workers with no email → can't join; surface to a manual list.
- Production: stronger identity key (govt-ID hash, DOB hash) needed to dedup across email-style aliases.
2Headcount on a date
Filter
Prompt: Given a list of workers with hire_date and optional termination_date, return headcount as of a target date. Handle the inclusive-end convention.
Show solution
from datetime import date
def headcount_on(workers: list[dict], as_of: date, term_inclusive: bool = True) -> int:
"""Count workers active on `as_of`.
term_inclusive=True means termination_date == as_of still counts as active
(the standard convention: last day worked is paid).
"""
n = 0
for w in workers:
hd = w["hire_date"]
td = w.get("termination_date")
if hd > as_of:
continue
if td is None:
n += 1
elif term_inclusive and td >= as_of:
n += 1
elif not term_inclusive and td > as_of:
n += 1
return n
Complexity: O(n). State the inclusive-end choice explicitly in your answer.
3Attrition rate
Arithmetic
Prompt: Given workers and a year, compute annualized attrition rate. Define your denominator (average of start and end headcount).
Show solution
from datetime import date
def attrition_rate(workers: list[dict], year: int) -> dict:
start = date(year, 1, 1)
end = date(year, 12, 31)
start_hc = headcount_on(workers, start)
end_hc = headcount_on(workers, end)
terms = sum(
1 for w in workers
if w.get("termination_date")
and start <= w["termination_date"] <= end
)
avg_hc = (start_hc + end_hc) / 2
return {
"terminations": terms,
"start_headcount": start_hc,
"end_headcount": end_hc,
"avg_headcount": avg_hc,
"attrition_rate": terms / avg_hc if avg_hc else None,
}
Discuss alternative denominators (start-only, including new hires). Senior signal: "There's no universal definition — I'd confirm convention with People Analytics before reporting."
4Payroll-period boundary detection
Date math
Prompt: Given a country's payroll cycle (monthly with cutoff 5 business days before end of month), a proposed effective date, and today's date, return: which payroll cycle this lands in, and whether we're inside the lock window.
Show solution
from datetime import date, timedelta
from calendar import monthrange
def is_business_day(d: date) -> bool:
return d.weekday() < 5 # 0=Mon, 4=Fri (no holiday calendar here; in prod, use one)
def shift_business_days(d: date, days_back: int) -> date:
"""Return the date that is `days_back` business days before d."""
cur = d
n = days_back
while n > 0:
cur -= timedelta(days=1)
if is_business_day(cur):
n -= 1
return cur
def payroll_window(today: date, effective: date,
country: str = "US",
lock_business_days_before_eom: int = 5) -> dict:
"""For a monthly payroll cycle with cutoff `lock_business_days_before_eom`
business days before the end of the month."""
target_month = effective.month
target_year = effective.year
last_day = date(target_year, target_month, monthrange(target_year, target_month)[1])
cutoff = shift_business_days(last_day, lock_business_days_before_eom)
locked = today > cutoff
cycle = f"{target_year}-{target_month:02d}"
return {
"cycle": cycle,
"cutoff": cutoff,
"locked": locked,
"days_to_cutoff": (cutoff - today).days,
}
Discuss: production version uses a real holiday calendar per country; this is the skeleton.
5Workday-EIB-style flat-file transformation
Transform
Prompt: Given a list of proposed job-change dicts, emit an EIB-ready CSV with exact column order, mandatory fields, ISO-date strings, and one comment column that concatenates rationale and source-run-id for traceability.
Show solution
import csv, io
from datetime import date
EIB_COLUMNS = [
"Employee_ID", "Effective_Date", "New_Position_ID",
"New_Job_Profile", "New_Supervisor_ID", "New_Location",
"New_FTE", "Reason_Code", "Business_Process_Comment",
]
REQUIRED = {"Employee_ID", "Effective_Date", "Reason_Code"}
def to_eib_csv(proposals: list[dict], run_id: str) -> str:
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=EIB_COLUMNS, extrasaction="ignore")
writer.writeheader()
for p in proposals:
row = {
"Employee_ID": p["employee_id"],
"Effective_Date": p["effective_date"].isoformat()
if isinstance(p["effective_date"], date) else p["effective_date"],
"New_Position_ID": p.get("new_position_id", ""),
"New_Job_Profile": p.get("new_job_profile", ""),
"New_Supervisor_ID": p.get("new_supervisor_id", ""),
"New_Location": p.get("new_location", ""),
"New_FTE": f"{p['new_fte']:.4f}" if p.get("new_fte") is not None else "",
"Reason_Code": p["reason_code"],
"Business_Process_Comment": f"{p.get('rationale','')} [run:{run_id} proposal:{p['proposal_id']}]",
}
missing = [k for k in REQUIRED if not row[k]]
if missing:
raise ValueError(f"Missing required EIB fields {missing} for {p}")
writer.writerow(row)
return buf.getvalue()
Discuss: header text and column order are part of the Workday EIB template contract — drifting from either breaks the upload. Production: snapshot-test the output against a canonical EIB header.
6Idempotency-key design for hire-rehire scenarios
Idempotency
Prompt: Design an idempotency-key scheme for "hire" proposals such that (a) retries of the same logical hire collapse to one effect, (b) a rehire of the same person 3 years later does not collapse with the original.
Show solution
The key encodes the intent of this hire event, not just the person:
import hashlib
def hire_idempotency_key(applicant_id: str,
hire_date: str,
position_id: str,
intent_window_days: int = 7) -> str:
"""A hire proposal is the same logical event when:
- same applicant
- same intended position
- hire_date within `intent_window_days` of original
Implementation: bucket the hire_date into `intent_window_days` periods so
near-equal dates collide but distant dates don't.
"""
from datetime import date
hd = date.fromisoformat(hire_date)
bucket = hd.toordinal() // intent_window_days
raw = f"hire:{applicant_id}:{position_id}:bucket={bucket}"
return hashlib.sha256(raw.encode()).hexdigest()[:24]
Why this works:
- Two retries of "hire applicant ASH-1 to position P-99 effective 2026-06-01" produce the same key.
- A retry with hire_date shifted by 2 days (operator edit) still produces the same key — the intent didn't change.
- A rehire 3 years later — same applicant, possibly same position — produces a different bucket, different key. No collision.
- A rehire to a different position is trivially a different key.
Articulating the business intent the key represents (not just "hash the request") is what distinguishes this answer. Mention the failure modes of naïve hashing (any field change creates a new key, defeating retry safety) and over-collapsing (rehire silently no-ops).
7Tenure-aware leave accrual
Date math
Prompt: Compute accrued PTO days for a worker from hire date through today, given: 1.25 days/month for first year, 1.5 days/month thereafter, with a 30-day cap. Pro-rate the month they were hired in.
Show solution
from datetime import date
from calendar import monthrange
def accrued_pto(hire_date: date, as_of: date,
rate_first_year: float = 1.25,
rate_after: float = 1.5,
cap_days: float = 30.0) -> float:
if as_of < hire_date:
return 0.0
total = 0.0
# First (possibly partial) month
first_month_days = monthrange(hire_date.year, hire_date.month)[1]
days_in_first = first_month_days - hire_date.day + 1
fraction = days_in_first / first_month_days
rate = rate_first_year # they're definitely < 1y at hire
total += rate * fraction
cur = date(hire_date.year, hire_date.month, 1)
# Advance to next month
cur = (date(cur.year + (cur.month // 12), (cur.month % 12) + 1, 1))
while cur <= as_of:
# Is this month < 12 months of tenure?
anniv = date(hire_date.year + 1, hire_date.month, hire_date.day)
rate = rate_first_year if cur < anniv else rate_after
# Was this month complete by as_of?
last_day = date(cur.year, cur.month, monthrange(cur.year, cur.month)[1])
if as_of >= last_day:
total += rate
else:
partial = (as_of.day) / monthrange(cur.year, cur.month)[1]
total += rate * partial
# Next month
cur = date(cur.year + (cur.month // 12), (cur.month % 12) + 1, 1)
return min(total, cap_days)
Discuss: real plans have many wrinkles (carryover, jurisdictional minimums, accrual on unpaid leave). State you're solving the named version; production: configurable plan model.
8Org reorg cascade
Graph
Prompt: A supervisory org tree is a forest of parent → children. Given a worker's old manager and new manager, compute every worker whose "skip-level chain" changed.
Show solution
from collections import deque
def descendants(children: dict[str, list[str]], root: str) -> set[str]:
"""All workers under `root` inclusive, BFS."""
seen = {root}
q = deque([root])
while q:
cur = q.popleft()
for c in children.get(cur, []):
if c not in seen:
seen.add(c)
q.append(c)
return seen
def affected_by_reorg(children: dict[str, list[str]],
moving_worker: str) -> set[str]:
"""Every worker whose skip-level chain changed when `moving_worker` (and
everyone reporting under them) moves. That's the subtree rooted at the
moving worker."""
return descendants(children, moving_worker)
Discuss: the "skip-level chain changed" set is the moved subtree, since their direct chain to root changes. If the question is about who reports to whom changes, it's the same subtree minus the moving worker (whose direct manager changes, plus everyone whose skip-level shifts).
9Overlapping leave windows
Sort + sweep
Prompt: Given a list of leave records per worker (start, end, type), detect any overlaps for the same worker. Return overlapping pairs.
Show solution
from collections import defaultdict
def detect_leave_overlaps(leaves: list[dict]) -> list[tuple[dict, dict]]:
by_worker = defaultdict(list)
for l in leaves:
by_worker[l["worker_id"]].append(l)
overlaps = []
for worker_id, lst in by_worker.items():
lst.sort(key=lambda x: (x["start"], x["end"]))
for i in range(1, len(lst)):
# Overlap if current.start <= prev.end
if lst[i]["start"] <= lst[i-1]["end"]:
overlaps.append((lst[i-1], lst[i]))
return overlaps
Complexity: O(n log n) per worker for sort. Edge: same-day adjacent windows — clarify inclusive/exclusive end-date convention.
10Active-day count over a range
Interval
Prompt: Given a worker's hire and termination dates, count how many days they were active within a given date range [range_start, range_end].
Show solution
from datetime import date
def active_days_in_range(hire: date, term: date | None,
range_start: date, range_end: date) -> int:
if range_end < range_start:
return 0
start = max(hire, range_start)
end = min(term, range_end) if term else range_end
if end < start:
return 0
return (end - start).days + 1 # inclusive both ends
Useful for FTE-weighted headcount, payroll period proration, and accrual calculations.