Better Rails Performance with JSON Patch

Nov 19, 2017

4 min read

FormAPI is a service that generates PDFs. The backend is written with Ruby on Rails,
and the PDF template editor is built using React. We use Postgres, and store the template’s field data in a jsonb column.

The first version of our template editor used a naïve approach when saving field data. Whenever the fields changed, we would just post the entire array to the server. This was working fine for our MVP and initial launch, but then a customer started setting up a PDF template with almost 500 fields. We started seeing alerts for requests that were taking around 5-10 seconds.

The obvious solution was to only send changes to the server. We use Redux in our React app, so I thought about sending the Redux actions. This wasn’t an option for FormAPI, because our Redux actions include some complicated logic. I didn’t want to rewrite that code in Ruby, but if we were using Node.js, then we might have been able to re-use the same JavaScript on the server.

The other option is to diff the plain JSON objects, and send the diff to the server. We can use JSON Patch for that:

JSON Patch is a format for describing changes to a JSON document. It can be used to avoid sending a whole document when only a part has changed.

I think it’s almost always better to use an IETF standard, instead of
inventing your own thing1. I used the following open source libraries:

classTemplate<ApplicationRecord# If there are any errors, we store them in here,# and add them during validationattr_accessor:json_patch_errorsvalidate:add_json_patch_errorsafter_save:clear_json_patchesattr_reader:fields_patch# The patch is applied as soon as this method is called.deffields_patch=(patch_data)# In case we want to access it later.@fields_patch=patch_dataself.json_patch_errors||={}json_patch_errors.delete:fieldsunlesspatch_data.is_a?(Array)json_patch_errors[:fields]='JSON patch data was not an array.'returnendhana_patch=Hana::Patch.new(patch_data)beginhana_patch.apply(fields)rescueHana::Patch::Exception=>exjson_patch_errors[:fields]="Could not apply JSON patch to \"fields\": #{ex.message}"endend# Clear any JSON patches and errors when reloading datadefreloadsuperclear_json_patchesendprivatedefadd_json_patch_errorsreturnunlessjson_patch_errors.present?json_patch_errors.eachdo|attribute,errors|errors.add(attribute,errors)endenddefclear_json_patches@fields_patch=nilself.json_patch_errors=nilendend

You could copy these RSpec tests
to make sure your implementation is correct. We might also release this as a gem at some point.

This change was just a few lines of code, but now we are sending much less data.

Another advantage of JSON Patch is that it’s much easier to support concurrent editing
with multiple users. It is often possible to apply patches in any order, especially when they only
contain replace or insert operations. If there is a conflict, then you can just reload
the latest data and let the user try again. You can also keep all the clients in sync
by using websockets to send patches from the server to the browser.