Tablematic: A tale of server-driven UI

When life gives you dynamic, custom user interfaces, your only sustainable option is to use a server-driven UI architecture. Here's how we did it at Routable.

Like all professionals, finance pros get used to working with their tools in specific ways. As part of Routable’s commitment to building the best-in-industry two-way data sync with accounting software, we also sync critical user interface elements into our payment and invoicing user interface. Making this happen has presented some serious technical challenges. Emily, a Front-end Engineer here at Routable, described building their solution as “the most challenging single feature of [her] entire career.” So without further ado, let’s find out what’s so difficult about replicating accounting software’s UI, and how we solved it.

The Challenges

For Routable’s software integrations with QuickBooks Online and Xero, we went with the simplest solution of more or less hard-coding support for each ledger’s user interface. It wasn’t quite so simple because each team’s accounting application configuration is its own special snowflake, and we had to hide and show some fields based on that configuration. At the time, this was the right move as it let us get out to market faster and avoid the pitfalls of building a poor abstraction. We followed the rule of 3:

“a reusable component should be tried out in three different applications before it will be sufficiently general to accept into a reuse library.”

Once we started to move into the enterprise space with our Oracle NetSuite integration, things got a lot more complicated. You see, with NetSuite you can add custom fields to any entity in the system and specify where in the user interface that field should appear. For example, imagine you ran a business where social media influencers were on the payroll – you could update the NetSuite customer form to include a secondary tab with fields for adding their social media handles.

When life gives you dynamic, custom user interfaces...

...your only sustainable option is to use a server-driven UI architecture (and then, of course, make lemonade). If you’re completely new to this concept, I’d recommend checking out Server Driven UI by Tom Lokhorst for an overview. At a high level though, you can think of it as a system where the server specifies what views the client should render via the JSON payload.

In order to customize the display, we didn’t have to diverge much from server-driven UI recommendations or examples you’ll find around the web. At the core, we implemented 6 different sections and over a dozen different fields that our backend application could inform our frontend to render. This is normally where you’d be done.

Complex forms

It’s somewhat straightforward to build a server-driven UI for a read-only interface or a simple form, but our implementation needs to drive Routable’s payable and receivable creation forms, which are… complex. In many ways, they’re closer to an Excel spreadsheet than a typical web form, because of our dependent fields, lookup fields, validations and custom formatting rules.

This is where the difficulty compounded. Going forward, we don’t want to hard-code any of these interdependencies, so we have to assume the client has no knowledge of any data fields or any of the interactions between them, and instead, find a way to share this logic from the backend to our frontend. For that we were able to turn to an excellent spec called JsonLogic which lets us encode logic in something akin to Lisp’s S-expressions.

“Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.”

Before we go further, let’s take a quick look at a sample data structure that comes down from our backend and how it maps to the form we render. Unlike Excel where we have a simple grid, we must use a tree structure to define our interface.

Field references

Remember how I said our form is similar to a spreadsheet? In a spreadsheet, you can reference other fields and perform calculations on them.

We have set things up so we can reference values in the tree with dot notation: {root}.main.general.invoice_number.

  • {root} references the root of our scope tree

  • {self} references the enclosing table row’s scope (this is useful for line items)

  • {value} references the current field’s value

Sharing Logic

  • Disabling form submissions on negative totals

    One of our simple examples of JSON Logic is how we use it to disable our form submission button when there is a negative total. Even though this is pretty basic, it’s critical for our backend to send this logic down to us because from the perspective of the frontend we have no idea there is even a totals section in our form, let alone what it’s called or that it’s value must be greater than zero.

For this, we send down the following simple rule which evaluates to something like can_submit = total > 0.

{
  "can_submit":{
    "rules":{
      ">":[
        {
          "var":"{root}.footer.totals.totals_total"
        },
        "0"
      ]
    }
  }
}
Copy
Field Dependencies

A more complicated use case is handling dependent fields. For example, with our NetSuite integration setting an amortization schedule is not required unless you also set an amortization start date, end date or residual. To handle this, we send down the following rule which evaluates to something like: required = amortization_start_date or amortization_end_date or amortization_residual.

{
  "required":{
    "rules":{
      "if":[
        {
          "or":[
            {
              "var":"{self}.extended_amortization_start_date"
            },
            {
              "var":"{self}.extended_amortization_end_date"
            },
            {
              "var":"{self}.extended_amortization_residual"
            }
          ]
        },
        true,
        false
      ]
    }
  }
}
Copy
Custom Formatting

For our last example, I’ll show you how we format dates. To support this, we added a custom operator to JSONLogic called format_date which… you guessed it formats a date in a specific way for our frontend.

{
  "value":{
    "rules":{
      "format_date":[
        {
          "var":"{value}"
        },
        "YYYY-MM-DD"
      ]
    }
  }
}
Copy

Of course, this is really just scratching the surface of our implementation: we had to add about a dozen operators to JSONLogic to support our use case including overriding their basic math operators because JS math is weird.

Shipping went smoothly

Emily handed this project off to our QA team to go crazy on before release and for the first time ever in recorded software history a complex feature like this made it through QA without bugs (and we have a great QA team)! To be clear, she admits there have been bugs introduced and discovered since… but hey, let her have her moment.

And while we’re letting people have their moments, we would be remiss not to mention the huge contributions of our resident transformer and lead backend architect Justin Myers. Justin is always an (Optimus) prime candidate for a good challenge and he spent countless hours working with Emily designing the schema and transforming accounting configurations into server-driven UI payloads.

We shipped tablematic to all our customers and it’s generally been a rip-roaring success. It’s not uncommon to hear prospective customers gasp when they see it for the first time in a sales call – “Wow, you make NetSuite look good!”

Yes we do, yes we do.

Interested in working at Routable on complex problems? Join our team – we’re hiring.

Developer

Recommended Reading

Developer

From 5 engineers to 50: What a fast-growth team has taught me

A Routable manager shares lessons learned as part of an engineering team that has exploded in size in a short time.

Developer

How business payments are like pull requests

Business payments are a lot like pull requests—the mechanism software engineers use to alert their team about changes to code and get it reviewed before it’s deployed.