Adapting automated real estate appraisals to new markets

September 3, 2022

I currently work at a tech-enabled real estate startup called Bowery Valuation. The company operates as a high-margin appraisal firm, enabled by an in-house web app that its appraisers use to automate and organize the appraisal process. It's like turbo-tax for real estate appraisals, with pages of interconnected forms that guide the appraiser through the appraisal process while computing the valuation of a property and generating the text of the appraisal report. The company's bread-and-butter revenue source is mixed-use multifamily buildings in the New York metro.

Because most mixed-use multifamily sales are valued in a similar fashion to one another, Bowery's web app has a good deal of automation built in for generating these reports, and its appraisers can complete appraisals on these properties more efficiently than its competitors, leading to higher operating margins. But just as turbo-tax loses its edge as an individual's taxes become more complicated, Bowery's appraisers are less efficient when using the web app to appraise different types of properties. The same problem exists for properties outside of the New York metro, where local business practices and state regulations require different types of information in a given appraisal. Some properties, such as marinas and golf courses, are appraised infrequently enough that there is little value in creating new features to speed up the appraisal process for these specific property types within Bowery's web app. On the other hand, there are key growth opportunities in efficiently appraising common property types such as warehouses and office buildings. Furthermore, we want to enable appraisers across the US to appraise properties on the app as easily and efficiently as appraisers in our home market of New York.

To lay the groundwork for this business objective, I recently lead a project to make Bowery's web app more flexible for different property types and new geographies. The work addressed the fundamental unit of an appraisal: square footage. As it turns out, different properties types and different regions of the US call for different methods of measuring square footage in an appraisal. The standard way to measure it in a mixed-use multifamily is with gross building area (GBA), which is the sum of the footprints of each story of the building. The footprint is calculated from the exterior walls of the building, so GBA is strictly greater than the inhabitable interior area of any given building. Other common metrics used in appraisals include gross leasable area (GLA), net rentable area (NRA), and net leasable area (NLA).

This project had 3 requirements:

  1. enable appraisers to select from a list of square footage metrics to base their appraisal on, specifically GBA, GLA, NRA, and NLA, while maintaining GBA as the default option
  2. update computed values throughout the appraisal that depend on the value of the square footage
  3. update generated text in the appraisal report to reflect the square footage metric being used

Before jumping into the details of how I created this feature, let me give a broad overview of what the system architecture looks like. We have a monolithic app with four different teams committing changes to it. The front end is written in javascript (using react) and is a series of interrelated forms organized into different sections that map roughly to the sections of an appraisal report. There's one section for basic property information, such as the address and square footage, and another section for recording the occupancy status, rent, and additional information about the building's units (if applicable). There's also a section for quickly identifying comparable sales, another for building a valuation based on those comps, and another for building a valuation based on the projected cash flow of the property. The frontend talks to a NodeJS backend with an Express API (javascript), and we're in the process of migrating it to NestJS (typescript). We use a MongoDB database, and our ODM models (mongoose) are fully migrated to NestJS, which means they have the added type-safety that comes with typescript. The backend also talks with various public APIs to automatically collect property information from state- and city-run data sources like New York City's PLUTO (Primary Land Use Tax Lot Output).

The forms the appraiser fills out in the app produce a MongoDB document associated with that appraisal. The document is continually updated and returned to the frontend as the appraiser saves their progress, where its value is held in the client's state using redux. It looks something like this:

{
    _id: object id
    report_generated: timestamp
    report_updated: timestamp
    appraiserId: int
    clientId: int
    latitude: double (i.e. floating-point number)
    longitude: double
    street1: string
    street2: string
    city: string
    state: string
    zip: int
    ...
    ...
    ...
    GBA: int
    ...
    ...
    ...
}

As you can imagine, there's much more information contained in an appraisal than I'm showing here, including many nested documents containing details about the property and the process through which multiple valuations of the property are determined, as well as custom text the appraiser wants to include in their appraisal report. In this example, the GBA (gross building area) lives at the top level of the document object, although in practice it lives in a nested document. When the appraiser finishes filling out all the required forms in the app, they'll see a button prompting them to generate a report. This sends the appraisal document to an appraisal-generation service written in C# that outputs a formatted word document as the (nearly) final deliverable for the appraisal client.

Alright, so now that we have the architecture out of the way, let's jump into the details of how we implemented this feature. The first challenge was deciding how to store different square footage metrics in the mongoDB document, keeping in mind our intention to design the feature to easily accommodate additional square footage metrics in the future.

The final changes looked something like this:

{
    id: object id
    report_generated: timestamp
    report_updated: timestamp
    appraiserId: int
    clientId: int
    latitude: double
    longitude: double
    street1: string
    street2: string
    city: string
    state: string
    zip: int
    ...
    ...
    ...
    GBA: int
    squareFootage: int
    squareFootageMetric: string (default: 'GBA')
    ...
    ...
    ...
}

We added two new fields: the integer squareFootage and the string squareFootageMetric. You might be wondering why we didn't remove the GBA field while we were at it. The reason for this is that there are two calculations that rely on GBA in every appraisal, even if the valuation conclusions are based on some other square footage metric. These two values are peripheral to the main conclusions of an appraisal and the details behind them are beyond the scope of this post. That said, they added complexity to the project because they required the app's forms to always collect GBA, and to optionally collect either GLA, NRA, or NLA. Fortunately, having the GBA field present actually helped my team implement the code changes in a piecemeal fashion that avoided merge conflicts in areas of the code that other teams were working on.

As I mentioned earlier, our database models are already migrated to NestJS, which means they have the type safety provided by typescript. As a result, we could constrain the field squareFootageMetric to the following subset of the type string:

enum squareFootageMetrics {
    GBA = 'GBA'
    GLA = 'GLA'
    NRA = 'NRA'
    NLA = 'NLA'
};

In practice, we didn't actually use an enum. It's a typescript feature that I avoid. Much has been written on the topic, so I won't dive into the details here. Instead of using an enum, we constrained squareFootageMetric with a dynamic type generated from an object. The general consensus in the typescript community for working around the use of enums is as follows:

const squareFootageMetrics = {
    GBA: 'GBA',
    GLA: 'GLA',
    NRA: 'NRA',
    NLA: 'NLA',
} as const;

// the following generates type squareFootageMetricsType = 'GBA' | 'GLA' | 'NRA' | 'NLA'
type squareFootageMetricsType = typeof squareFootageMetrics[keyof typeof squareFootageMetrics];

Now that we had updated the data structure, I created a data migration to adjust existing records. It took every existing appraisal document, set appraisal.squareFootage to the value of the appraisal.GBA field and set appraisal.squareFootageMetric to 'GBA'. With the data migrated, the next step was project requirement #1: to enable appraisers to select from a list of square footage metrics to base their appraisal on. The existing app included a form field like the following:

GBA:

I created a list of radio buttons above this input asking the user to select the square footage metric they want to use for their appraisal. The default selection for the list is GBA, and the options are created dynamically from the array Object.values(squareFootageMetrics). Since squareFootageMetricsType is also created dynamically from squareFootageMetrics, we can easily add additional square footage metrics to the system in the future with a single line of code. If the user selects a radio button other than GBA, then an additional form field underneath GBA appears. The GBA input is required, and the second input is required if showing. It works like this:

Basis of square footage:
GBA:

When the page is saved, the radio button selection sets the value of appraisal.squareFootageMetric, the first text input sets the value of appraisal.GBA, and the second text input sets the value of appraisal.squareFootage. If the second text input is null (which occurs when the GBA radio button is selected), then appraisal.squareFootage is set to the same value as the appraisal.GBA.

The last stage of development, where the bulk of the code changes actually occurred, was to change ~30 values downstream of a property's square footage and the basis for determining it (GBA, NLA, etc.) in a given appraisal. My team uses a JIRA Kanban board to track and coordinate our work, so I created an individual ticket on the board for each required downstream change. Note that I also broke down the aforementioned work into tickets, and I classed all tickets for this project under the same epic.

For the downstream changes, there were 3 types of tickets:

  1. changes to the generated text in the outputted .docx appraisal report: these involved changing a hardcoded string such as 'GBA' to a dynamic template string like `${appraisal.squareFootageMetric}`.
  2. changes to a computed value: these involved changing a piece of frontend or backend code such as:
    const pricePerSquareFoot = appraisal.valuation / appraisal.GBA
    to:
    const pricePerSquareFoot = appraisal.valuation / appraisal.squareFootage
  3. changes to a computed value and a string value: these involved the process mentioned in #2 and an additional change to some associated frontend string value such as:
    "The unit's portion of total building area is unit area divided by GBA"
    to:
    `The unit's portion of total building area is unit area divided by ${appraisal.squareFootageMetric}`

As I mentioned earlier, because we didn't remove the appraisal.GBA field from the MongoDB document, we were able to implement these changes in a piecemeal fashion. Each change was isolated from the others, so we made a single branch and pull-request for each ticket. This made for quick turnaround times on code reviews and also enabled us to prioritize certain changes ahead of others. In some cases we would make a change to a specific form quickly if we knew another team was about to start working on that form. In other cases, we could wait a few days to touch a specific form while another team finished up and merged changes to that form. This naturally gave our team an incentive to review certain pull-requests quickly in order to speed up our own work. At this point in the course of the project I effectively stopped coding. My time was spent most effectively by coordinating my team's work around the work of other teams and by quickly reviewing the pull-requests my team members were opening.

Overall the process went smoothly. One of the hiccups we ran into along the way is that we found about 10 additional downstream values that needed to be changed. Our design team found about half of them and our engineers discovered the other half. I had already given estimates of our timeline before these tickets were added to the epic, but I was careful to communicate that strictly the engineering work (excluding QA, design, and product reviews) would be done for the existing tickets by a specific date This made it easy to update our self-imposed deadline when additional necessary changes were found. I just made ~10 new tickets and shared a new deadline for those tickets. Communicating this way turned out to be one of the most important decisions I made during the process as it prevented my team from getting stuck in a time crunch to hit a deadline.

Ultimately, the implementation of this feature is straightforward and easy to communicate. This is the aspect of the project I'm most pleased with. Without a well thought out plan for its architecture and implementation we easily could have ended up with a hacky, complex solution that would need reworking in the future. Bowery's web app now has a simple way for an appraiser to change the basis of a property's square footage calculation, and it has a trivially easy way for developers to further increase an appraiser's flexibility in this regard. This directly aids Bowery's operational efforts as it expands its business model to new appraisal types and geographies.


Special thanks to Shuvo Rahman, who helped plan the changes to the data structure.