Major Refactor: Snapshots

Snapshots are a set of loan and configuration data frozen in place at key times in the loan origination lifecycle. These key times are 1) when a loan is decisioned by an underwriter and 2) when it is contracted by a loan officer. Snapshots are a fundamental part of how the LOS functions, described by one developer as the columns in the our flagship FG software product in major need of a refactor.

Identify the problem

The problem was that we were treating snapshots as though they were second-class citizens:

  • snapshot business logic was spread across the application
  • all sorts of logic scattered all over the place to determine which snapshot type should be loaded
  • existing anti-patterns as a solution to old rigid snapshot structure implemented for decision/contracting and various states: see huge red flag method named getMigratedPreviousSnapshot
  • lacked a distinct source of truth as data was being overwritten in many places in the codebase
  • awful handling of state-specific business logic where some specific field should be loaded from a different snapshot
  • decision and contracting snapshot types could not be used interchangeably

The source of truth item described above caused a slough of overlapping bugs. This was what led to the decision that we had to stop everything and refactor snapshots.

Find a solution

We decided to tackle this major refactor in the following ways:

  • move much of the existing decision/contracting model and service functionality into a single snapshot service
  • create a new snapshot DB table and entity to replace old decision/contracting tables and entities and migrate the data
  • add structs to the snapshot data hydration layer to ensure a standard set of fields exist for all existing and future snapshots
  • pull business logic pertaining to snapshots from random models, controllers, and services and consolidate into the snapshot service
  • create hooks for adding business logic at carefully considered control points:
    • when fetching DB record via doctrine entity
    • during snapshot field hydration and structs
    • when mapping variables to the back-end viewmodel ingested by PHP templates
    • when mapping variables to the front-end viewmodel used by knockout bindings and other JavaScript functionality

By consolidating decision and contracting snapshots into a snapshot interface with a single DB table, we gained the ability to easily relate snapshots via a linked list. It used to be that some random service way over there had to know way too much of both state and context. That random service way over there used to say, “Oh, I’m going to definitely be mostly needing the decision snapshot and the contracting snapshot if it exists for this one piece of data here so I must load every class related to decision and contract and jump through all the method hoops that they require”.

Now following our refactor that random service has a variety of options. Hopefully the single snapshot dependency could be injected and because the snapshot is context aware by nature, it arrives with data prepared in some smart way true to the sense of the application as a whole and the edge case has already been addressed. Failing dependency injection, that random service could simply say, “Give me the last valid snapshot,” or “Give me the last valid decision,” or even, “Give me the decision snapshot that this contract is based upon” via the new snapshot generation helper methods.

This complex feat took a couple of stressful months, but was well worth the trouble.