DEV Community

Cover image for Building a Multi-Scenario Real Estate Feasibility Engine Without Duplicating the Model
Abdul Shamim
Abdul Shamim

Posted on

Building a Multi-Scenario Real Estate Feasibility Engine Without Duplicating the Model

A common way to implement scenario analysis is to create separate copies of a model for the base case, downside case, and upside case. This approach is easy to understand initially, but it becomes difficult to maintain as the model grows.

If calculation logic changes, every scenario must be updated. If one version uses a different formula accidentally, scenario comparison becomes unreliable. The problem becomes more serious in real estate feasibility because the model may contain monthly cash flows, construction expenditure, sales schedules, financing, interest, taxes, phase dependencies, and investment metrics.

A better architecture keeps the calculation engine separate from the scenario assumptions.

The core structure

The engine should have three layers:

A base project definition containing the underlying development assumptions.
Scenario overrides containing only the assumptions that change.
A calculation engine that processes every scenario using the same financial logic.
A simplified project definition might look like this:

base_project = {
    "land_cost": 20_000_000,
    "construction_cost": 70_000_000,
    "other_costs": 10_000_000,
    "sales_revenue": 130_000_000,
    "construction_months": 30,
    "sales_start_month": 12,
    "sales_duration_months": 36,
    "annual_discount_rate": 0.12
}
Enter fullscreen mode Exit fullscreen mode

The scenarios should not duplicate the entire project.

scenarios = {
    "base": {},

    "cost_pressure": {
        "construction_cost_multiplier": 1.10
    },

    "slow_sales": {
        "sales_duration_multiplier": 1.25


    },

    "combined_downside": {
        "construction_cost_multiplier": 1.10,
        "sales_revenue_multiplier": 0.95,
        "sales_duration_multiplier": 1.25
    }
}
Enter fullscreen mode Exit fullscreen mode

The base project remains the source of truth. Each scenario stores only the difference.

Resolve assumptions before calculation

Before the financial engine runs, the base project and scenario overrides should be combined into one resolved assumption set.

from copy import deepcopy

def resolve_scenario(base_project, overrides):
    project = deepcopy(base_project)

    construction_multiplier = overrides.get(
        "construction_cost_multiplier", 1.0
    )

    revenue_multiplier = overrides.get(
        "sales_revenue_multiplier", 1.0
    )

    duration_multiplier = overrides.get(
        "sales_duration_multiplier", 1.0
    )

    project["construction_cost"] *= construction_multiplier
    project["sales_revenue"] *= revenue_multiplier

    project["sales_duration_months"] = round(
        project["sales_duration_months"]
        * duration_multiplier
    )

    return project
Enter fullscreen mode Exit fullscreen mode

This gives every scenario a complete set of assumptions while preserving one common calculation engine.

The architecture matters because scenario analysis should change inputs, not formulas. If the downside scenario calculates financing or IRR differently from the base case, the comparison is no longer controlled.

Generate monthly construction expenditure

Real estate models usually require time-based cash flows. Total construction cost should therefore be distributed across the development programme.

For a simple demonstration, a normalized expenditure curve can be used.

construction_curve = [
    0.02, 0.03, 0.05, 0.07, 0.09,
    0.11, 0.13, 0.14, 0.13, 0.10,
    0.07, 0.04, 0.02
]
Enter fullscreen mode Exit fullscreen mode

The weights must sum to 1.

def validate_curve(curve):
    if abs(sum(curve) - 1.0) > 1e-9:
        raise ValueError(
            "Construction curve must sum to 1.0"
        )
Enter fullscreen mode Exit fullscreen mode

The monthly expenditure can then be calculated:

def distribute_cost(total_cost, curve):
    validate_curve(curve)

    return [
        total_cost * weight
        for weight in curve
    ]
Enter fullscreen mode Exit fullscreen mode

A production feasibility system would normally support more flexible expenditure profiles, different construction packages, phase-specific curves, and changes in programme duration. The basic principle remains the same: the scenario changes the assumptions, and the common engine recalculates the cash flow.

Model sales timing separately from total revenue

A frequent modelling error is to change total revenue without changing the timing of revenue.

Suppose the base case generates $130 million over 36 months. A slow-sales scenario may still generate the same nominal revenue but take 45 months.

Those scenarios should produce different IRR and NPV results because the cash arrives at different times.

A simple linear sales schedule can be generated as follows:

def generate_sales_cashflow(
    total_revenue,
    start_month,
    duration_months,
    model_months
):
    cashflow = [0.0] * model_months

    monthly_revenue = (
        total_revenue / duration_months
    )

    for month in range(
        start_month,
        min(
            start_month + duration_months,
            model_months
        )
    ):
        cashflow[month] = monthly_revenue

    return cashflow
Enter fullscreen mode Exit fullscreen mode

Real projects rarely sell linearly, so a production engine should support absorption curves. However, even this simplified implementation demonstrates an important distinction between changing the amount of revenue and changing when the revenue is received.

Calculate NPV using the correct period rate

If the model uses monthly cash flows and the discount rate is expressed as an effective annual rate, the monthly rate should be calculated as:

In Python:

def monthly_npv(cashflows, annual_rate):
    monthly_rate = (
        (1 + annual_rate) ** (1 / 12)
    ) - 1

    return sum(
        cashflow /
        ((1 + monthly_rate) ** month)
        for month, cashflow
        in enumerate(cashflows)
    )
Enter fullscreen mode Exit fullscreen mode

The annual rate should not automatically be divided by 12 unless the model is explicitly using a nominal annual rate convertible monthly. Financial engines should make the rate convention clear because small implementation choices can materially affect large, long-duration projects.

Keep scenario outputs structured

The result of each scenario should contain more than IRR and NPV.

scenario_result = {
    "scenario_name": "combined_downside",
    "total_cost": 117_000_000,
    "total_revenue": 123_500_000,
    "npv": -2_400_000,
    "irr": 0.087,
    "peak_funding": 54_000_000,
    "completion_month": 48
}
Enter fullscreen mode Exit fullscreen mode

This structure makes it possible to compare scenarios programmatically.

def compare_scenarios(results):
    return sorted(
        results,
        key=lambda result: result["npv"],
        reverse=True
    )
Enter fullscreen mode Exit fullscreen mode

More importantly, the system can identify why a scenario changed rather than merely showing a different return.

For example:

Base Case
IRR: 18.2%
Peak Funding: $41.0m

Combined Downside
IRR: 8.7%
Peak Funding: $54.0m

Primary Drivers
Construction cost: +10%
Sales revenue: -5%
Sales duration: +25%
Enter fullscreen mode Exit fullscreen mode

The explanation layer is important in financial software because users need to understand the assumptions responsible for the output.

Scenario combinations require careful design

If a system supports five alternative values for ten independent variables, a full Cartesian combination produces:

scenarios.

Running every possible combination is usually unnecessary and can make the results difficult to interpret.

A feasibility engine should distinguish between user-defined scenarios and sensitivity analysis.

A user-defined scenario represents a coherent view of the project, such as a base case, cost-pressure case, slow-sales case, or combined downside.

Sensitivity analysis isolates specific variables to measure how strongly the project responds to them.

These are related tools, but they answer different questions.

Why this architecture matters for real feasibility software

This type of separation between assumptions, scenarios, and calculation logic is directly relevant to platforms such as FeasibilityPro.ai.

The platform has to support different development structures, including plot-level residential projects, multi-phased master plans, and hospitality developments. A single hard-coded financial model is not sufficient because each project type contains different assumptions and calculation workflows.

At the same time, scenario comparison should not require the underlying financial logic to be duplicated every time a user wants to test a different cost, sales, financing, or development case.

The architectural principle is simple:

Project Data
     ↓
Scenario Overrides
     ↓
Resolved Assumptions
     ↓
Common Calculation Engine
     ↓
Cash Flows
     ↓
IRR / NPV / ROI / Peak Funding
     ↓
Scenario Comparison
Enter fullscreen mode Exit fullscreen mode

This structure improves maintainability and auditability. A formula change can be applied once. Every scenario then uses the updated logic.

The technical objective is not more scenarios

A good scenario engine is not measured by how many model versions it can generate.

Its purpose is to help users understand which assumptions change the investment outcome, how variables interact, and where the project becomes financially vulnerable.

For developers building financial or PropTech systems, this requires a clear separation between data, assumptions, calculation logic, and outputs. Without that separation, scenario modelling quickly becomes a collection of duplicated models that are difficult to test and maintain.

The calculation engine should remain stable.

The assumptions should change.

The outputs should explain the consequences.

That is the foundation of a scenario system that can be used for real development decisions.

Top comments (0)