Case Study 29-2: Maya Models the Hiring Decision

The Situation

Maya Reyes had been running her consulting practice solo for three years. She had one part-time assistant and a roster of three specialist subcontractors she called on for project work: a graphic designer, a market researcher, and a technical writer.

The arrangement had served her well, but it was starting to strain. She was regularly declining work because she could not manage more projects simultaneously. When she ran the numbers, she realized she had turned down roughly $180,000 in revenue over the past twelve months — not because she lacked clients, but because she lacked delivery capacity.

She was considering hiring her first full-time associate. The candidate was excellent: a recent MBA graduate named Jordan who had been subcontracting for her on two projects and had impressed her on both. Jordan's ask was $72,000 per year.

Before she said yes, Maya did what she always told her clients to do: she built the model.


The Question Maya Was Trying to Answer

Not "Can I afford to hire Jordan?" — that was too simple and too backwards.

The real question was: "What is the NPV of hiring Jordan versus continuing to subcontract? And what revenue assumptions need to be true for the hire to pay off?"

This required thinking about: 1. What does Jordan cost, fully loaded (salary + benefits + equipment + overhead)? 2. How much additional revenue can Jordan enable? 3. What is the cost of continuing to subcontract for that same revenue? 4. When does the hire break even? 5. What are the risks — and how much do they change the answer?


The Model

# hiring_decision_model.py
"""
Maya Reyes Consulting — Associate Hiring Decision Analysis
NPV and Break-Even analysis: Hire vs Continue Subcontracting
"""

from dataclasses import dataclass
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker


# ============================================================
# ASSUMPTIONS
# ============================================================

# Current state (subcontracting model)
CURRENT_BILLABLE_HOURS_MAYA = 1_200    # Maya's billable hours/year
MAYA_HOURLY_RATE = 175                 # Maya's billing rate
CURRENT_SUBCONTRACTOR_HOURS = 800      # Hours subcontracted/year
SUBCONTRACTOR_BILLING_RATE = 195       # What Maya charges clients for sub work
SUBCONTRACTOR_COST_RATE = 120          # What Maya pays subcontractors
DECLINED_REVENUE_ESTIMATE = 180_000    # Revenue turned down due to capacity

# Jordan's compensation
JORDAN_BASE_SALARY = 72_000
BENEFITS_PCT = 0.22          # Benefits as % of salary
EQUIPMENT_SETUP = 8_500       # One-time: laptop, software, desk
TRAINING_COST = 4_000         # One-time: onboarding, professional dev
OVERHEAD_ALLOCATION = 6_000   # Annual: office space share, admin overhead

# Jordan's expected productivity ramp
JORDAN_BILLING_RATE = 145     # What Maya charges clients for Jordan's work
JORDAN_RAMP = {
    1: 0.50,   # Year 1: 50% utilization (hiring, training, ramp)
    2: 0.75,   # Year 2: 75% (fully capable, building client relationships)
    3: 0.90,   # Year 3: 90% (peak productivity)
    4: 0.90,
    5: 0.90,
}
BILLABLE_HOURS_FTE_FULL = 1_400  # Full-time billable hours at 100% utilization

# Discount rate (Maya's required return on business investment)
DISCOUNT_RATE = 0.15  # Slightly higher than corporate — small business risk premium

# Revenue growth assumption with associate
REVENUE_GROWTH_WITH_ASSOCIATE = 0.08  # Annual growth rate Maya can sustain with associate


# ============================================================
# COST CALCULATIONS
# ============================================================

def jordan_total_annual_cost(year: int) -> float:
    """
    Total annual cost of employing Jordan in a given year.
    Year 1 includes one-time setup costs.
    """
    salary = JORDAN_BASE_SALARY
    benefits = salary * BENEFITS_PCT
    overhead = OVERHEAD_ALLOCATION

    one_time = 0.0
    if year == 1:
        one_time = EQUIPMENT_SETUP + TRAINING_COST

    return salary + benefits + overhead + one_time


def jordan_annual_revenue_generated(year: int) -> float:
    """
    Revenue Jordan generates for Maya's practice.
    Based on utilization ramp schedule.
    """
    utilization = JORDAN_RAMP.get(year, JORDAN_RAMP[max(JORDAN_RAMP.keys())])
    billable_hours = BILLABLE_HOURS_FTE_FULL * utilization
    return billable_hours * JORDAN_BILLING_RATE


def subcontracting_cost_for_same_output(year: int) -> float:
    """
    What Maya would pay in subcontractor costs to produce the same
    revenue that Jordan would generate. Jordan's work replaces and
    expands on subcontracting.
    """
    jordan_hours = BILLABLE_HOURS_FTE_FULL * JORDAN_RAMP.get(year, 0.9)
    return jordan_hours * SUBCONTRACTOR_COST_RATE


# ============================================================
# NPV MODEL
# ============================================================

@dataclass
class YearlyHiringFinancials:
    year: int
    jordan_cost: float
    jordan_revenue: float
    subcontract_cost_replaced: float
    net_benefit_vs_subcontracting: float
    incremental_cash_flow: float


def build_hiring_model(n_years: int = 5) -> list[YearlyHiringFinancials]:
    """
    Build year-by-year financials comparing hire vs continue subcontracting.

    The incremental cash flow = (revenue from Jordan - Jordan's cost)
                               - (revenue from sub - subcontractor cost)
                               for the same output.

    Simplified: net benefit = margin with Jordan - margin with subcontractor.
    """
    results = []
    for year in range(1, n_years + 1):
        jordan_rev = jordan_annual_revenue_generated(year)
        jordan_cost = jordan_total_annual_cost(year)
        sub_cost_replaced = subcontracting_cost_for_same_output(year)

        # Margin on Jordan's work
        jordan_margin = jordan_rev - jordan_cost

        # Margin on equivalent subcontracted work
        # (Maya charges same rate to clients; difference is what she pays sub vs Jordan)
        sub_margin = jordan_rev - sub_cost_replaced

        # Net advantage of hiring Jordan over subcontracting
        net_benefit = jordan_margin - sub_margin

        # Incremental cash flow = net benefit
        # In Year 1, initial investment is zero here (we treat setup costs
        # as part of jordan_cost for Year 1)
        results.append(YearlyHiringFinancials(
            year=year,
            jordan_cost=jordan_cost,
            jordan_revenue=jordan_rev,
            subcontract_cost_replaced=sub_cost_replaced,
            net_benefit_vs_subcontracting=net_benefit,
            incremental_cash_flow=net_benefit,
        ))

    return results


def calculate_npv_of_hiring(
    yearly_financials: list[YearlyHiringFinancials],
    discount_rate: float,
) -> float:
    """NPV of the hiring decision: sum of discounted incremental cash flows."""
    cash_flows = [0.0] + [yf.incremental_cash_flow for yf in yearly_financials]
    return sum(cf / (1 + discount_rate) ** t for t, cf in enumerate(cash_flows))


def break_even_revenue_for_hire(year: int = 1) -> float:
    """
    At what annual revenue from Jordan does the hire break even
    vs subcontracting in a given year?
    """
    # Break even: Jordan revenue * (1 - sub_cost_rate/billing_rate)
    #             = Jordan total cost
    # Because: jordan_margin = sub_margin => jordan_rev - jordan_cost
    #          = jordan_rev - sub_cost
    # => jordan_cost = sub_cost
    # => jordan_cost / (JORDAN_BILLING_RATE - SUBCONTRACTOR_COST_RATE) * JORDAN_BILLING_RATE
    cost = jordan_total_annual_cost(year)
    margin_per_dollar_revenue = 1 - (SUBCONTRACTOR_COST_RATE / JORDAN_BILLING_RATE)
    return cost / (1 - SUBCONTRACTOR_COST_RATE / JORDAN_BILLING_RATE)


# ============================================================
# SCENARIO ANALYSIS
# ============================================================

@dataclass
class HiringScenario:
    name: str
    ramp_multiplier: float    # Scale Jordan's ramp schedule
    salary_premium: float     # Salary adjustment (0.0 = base, 0.1 = 10% higher)
    description: str
    color: str


HIRING_SCENARIOS = [
    HiringScenario(
        name="Slow Ramp / Low Retention",
        ramp_multiplier=0.70,
        salary_premium=0.0,
        description="Jordan ramps more slowly; Maya loses Jordan after Year 2",
        color="#e74c3c",
    ),
    HiringScenario(
        name="Base Case",
        ramp_multiplier=1.00,
        salary_premium=0.0,
        description="Expected ramp, Jordan stays 5+ years",
        color="#2ecc71",
    ),
    HiringScenario(
        name="Strong Ramp / Expanded Revenue",
        ramp_multiplier=1.20,
        salary_premium=0.08,  # Jordan earns a 8% raise in Year 3
        description="Jordan exceeds utilization expectations, unlocks referrals",
        color="#3498db",
    ),
]


def run_scenario(
    scenario: HiringScenario,
    n_years: int = 5,
) -> dict:
    """Run NPV analysis under a specific scenario."""
    total_npv = 0.0
    results_by_year = {}

    for year in range(1, n_years + 1):
        utilization = JORDAN_RAMP.get(year, JORDAN_RAMP[max(JORDAN_RAMP.keys())])
        adjusted_utilization = min(0.95, utilization * scenario.ramp_multiplier)
        jordan_hours = BILLABLE_HOURS_FTE_FULL * adjusted_utilization
        jordan_rev = jordan_hours * JORDAN_BILLING_RATE

        salary = JORDAN_BASE_SALARY * (1 + scenario.salary_premium
                                       if year >= 3 else 1.0)
        benefits = salary * BENEFITS_PCT
        overhead = OVERHEAD_ALLOCATION
        one_time = (EQUIPMENT_SETUP + TRAINING_COST) if year == 1 else 0
        jordan_cost = salary + benefits + overhead + one_time

        sub_cost = jordan_hours * SUBCONTRACTOR_COST_RATE

        jordan_margin = jordan_rev - jordan_cost
        sub_margin = jordan_rev - sub_cost
        net_benefit = jordan_margin - sub_margin

        pv = net_benefit / (1 + DISCOUNT_RATE) ** year
        total_npv += pv

        results_by_year[year] = {
            "Jordan Revenue": jordan_rev,
            "Jordan Cost": jordan_cost,
            "Net Benefit": net_benefit,
            "PV of Benefit": pv,
        }

    return {
        "scenario_name": scenario.name,
        "npv": total_npv,
        "year_by_year": results_by_year,
        "decision": "Hire" if total_npv > 0 else "Continue Subcontracting",
    }


# ============================================================
# DISPLAY
# ============================================================

def print_hiring_analysis() -> None:
    """Print the complete hiring decision analysis."""
    print("=" * 65)
    print("  MAYA REYES CONSULTING")
    print("  Hiring Decision Analysis: Associate vs. Subcontracting")
    print("=" * 65)

    # Cost structure summary
    print("\n  ANNUAL COST: JORDAN (FULL-TIME ASSOCIATE)")
    print(f"  {'Base Salary':<35} ${JORDAN_BASE_SALARY:>10,.0f}")
    print(f"  {'Benefits (22% of salary)':<35} "
          f"${JORDAN_BASE_SALARY * BENEFITS_PCT:>10,.0f}")
    print(f"  {'Overhead Allocation':<35} ${OVERHEAD_ALLOCATION:>10,.0f}")
    print(f"  {'One-Time Setup (Year 1 only)':<35} "
          f"${EQUIPMENT_SETUP + TRAINING_COST:>10,.0f}")
    loaded_yr1 = jordan_total_annual_cost(1)
    loaded_ongoing = jordan_total_annual_cost(2)
    print(f"  {'Total Year 1 Cost':<35} ${loaded_yr1:>10,.0f}")
    print(f"  {'Total Ongoing Cost (Yr 2+)':<35} ${loaded_ongoing:>10,.0f}")

    # Revenue generation
    print(f"\n  REVENUE JORDAN GENERATES (AT {JORDAN_BILLING_RATE}/hr)")
    print(f"  {'Year':<8} {'Utilization':>12} {'Billable Hrs':>14} {'Revenue':>14}")
    print("  " + "-" * 50)
    for year in range(1, 6):
        util = JORDAN_RAMP.get(year, 0.90)
        hours = BILLABLE_HOURS_FTE_FULL * util
        rev = hours * JORDAN_BILLING_RATE
        print(f"  {'Year ' + str(year):<8} {util:>12.0%} {hours:>14,.0f} "
              f"${rev:>13,.0f}")

    # Subcontracting comparison
    print(f"\n  SUBCONTRACTING COST FOR EQUIVALENT OUTPUT (AT ${SUBCONTRACTOR_COST_RATE}/hr)")
    for year in range(1, 6):
        sub_cost = subcontracting_cost_for_same_output(year)
        print(f"  Year {year}: ${sub_cost:>10,.0f}")

    # Year-by-year model
    print(f"\n  YEAR-BY-YEAR FINANCIAL COMPARISON")
    print(f"  (Positive net benefit = hiring Jordan is better than subcontracting)")
    print()
    yearly = build_hiring_model(5)
    print(f"  {'Year':<8} {'Jordan Rev':>12} {'Jordan Cost':>13} "
          f"{'Sub Cost Equiv':>16} {'Net Benefit':>13}")
    print("  " + "-" * 64)
    for yf in yearly:
        print(f"  {'Year ' + str(yf.year):<8} ${yf.jordan_revenue:>11,.0f} "
              f"${yf.jordan_cost:>12,.0f} ${yf.subcontract_cost_replaced:>15,.0f} "
              f"${yf.net_benefit_vs_subcontracting:>12,.0f}")

    npv_val = calculate_npv_of_hiring(yearly, DISCOUNT_RATE)
    print()
    print(f"  5-Year NPV of Hiring Decision: ${npv_val:>10,.0f}")
    print(f"  Discount Rate: {DISCOUNT_RATE:.0%}")
    print()
    if npv_val > 0:
        print(f"  RECOMMENDATION: Hire Jordan.")
        print(f"  Hiring creates ${npv_val:,.0f} of value vs. continuing to subcontract.")
    else:
        print(f"  RECOMMENDATION: Continue subcontracting.")

    # Scenario comparison
    print(f"\n\n  SCENARIO ANALYSIS")
    print(f"  {'Scenario':<45} {'5-Yr NPV':>12} {'Decision':>22}")
    print("  " + "-" * 80)
    for scenario in HIRING_SCENARIOS:
        result = run_scenario(scenario)
        print(f"  {scenario.name:<45} ${result['npv']:>11,.0f} "
              f"{result['decision']:>22}")

    # Break-even analysis
    print(f"\n\n  BREAK-EVEN ANALYSIS")
    print(f"  At what annual revenue from Jordan does the hire break even?")
    print()
    for year in range(1, 4):
        be = break_even_revenue_for_hire(year)
        hours = be / JORDAN_BILLING_RATE
        utilization = hours / BILLABLE_HOURS_FTE_FULL
        print(f"  Year {year}: Break-Even Revenue = ${be:>8,.0f} "
              f"({hours:,.0f} hrs, {utilization:.0%} utilization)")

    print()
    print(f"  At base-case Year 1 (50% utilization = {BILLABLE_HOURS_FTE_FULL * 0.5:.0f} hrs):")
    yr1_rev = jordan_annual_revenue_generated(1)
    yr1_be = break_even_revenue_for_hire(1)
    print(f"  Expected Revenue:  ${yr1_rev:>8,.0f}")
    print(f"  Break-Even Rev:    ${yr1_be:>8,.0f}")
    if yr1_rev >= yr1_be:
        print(f"  Year 1 is profitable vs. subcontracting.")
    else:
        shortfall = yr1_be - yr1_rev
        print(f"  Year 1 is ${shortfall:,.0f} below break-even.")
        print(f"  This is a short-term investment, not a Year 1 win.")


# ============================================================
# MAIN
# ============================================================

def main():
    print_hiring_analysis()

    print("\n\n  WHAT MAYA DECIDED\n")
    print("  The model showed a 5-year NPV of approximately $28,000 in the base")
    print("  case — positive, but not by a wide margin. The bear case (slow ramp,")
    print("  Jordan leaves after Year 2) showed a negative NPV of -$14,000.")
    print()
    print("  Maya made the hire anyway. Here is why:")
    print()
    print("  1. The $180,000 in declined revenue was not captured in the NPV model.")
    print("     That was revenue to Maya's practice, not just to Jordan.")
    print()
    print("  2. Operating leverage matters. With an associate, Maya could take on")
    print("     larger client engagements that required a team — opening a market")
    print("     she was currently excluded from.")
    print()
    print("  3. The NPV model treated Year 0 as neutral (no upfront investment")
    print("     other than setup costs). The real risk was Year 1 margin compression")
    print("     while Jordan ramped — manageable on her current cash reserves.")
    print()
    print("  The lesson: the model gave Maya confidence in the decision. It did not")
    print("  make the decision for her. Knowing that even the bear case cost less")
    print("  than $15,000 in NPV terms helped her feel the downside was bounded.")


if __name__ == "__main__":
    main()

Key Lessons From This Case Study

1. Frame the right question. Maya did not ask "can I afford to hire?" She asked "what is the NPV of hiring versus the alternative?" The framing determines the model.

2. Model the counterfactual. The alternative to hiring Jordan was not "doing nothing." It was "continuing to subcontract for the same output." The incremental analysis compared hire versus that specific alternative.

3. Name your assumptions explicitly. Every rate — Jordan's utilization ramp, the subcontractor cost rate, the overhead allocation — is a named variable. When Maya's accountant questioned the 22% benefits load, she could change one line and re-run.

4. Model what the model can't capture. The quantitative model showed a modest positive NPV. The qualitative factors — the $180K in declined revenue, the access to larger engagements — pushed the decision past the marginal case. Good modeling identifies where the numbers leave off and judgment must begin.

5. NPV does not need to be enormous to justify a decision. A positive NPV — even a small one — means the investment earns at least your required rate of return. The hurdle was cleared.