Cannibalizing the Monolith: A Micro-Application, Component-Driven Approach to Web Development
Part 2: Creating a Standalone Micro-Application
If you haven’t already, please check out Part 1, where we discussed our approach to shipping our first React/Redux components to production.
Our React + Redux based approach to upgrading our legacy website had gained a good deal of momentum. Our web team was able to rapidly apply updates across our site and we quickly developed the confidence to take on a bigger challenge, replacing an entire legacy user flow with a new, standalone single-page application (SPA). We chose to opportunistically pair this desire with a top ask on our product roadmap: rebuild all our user payments page, flows, and integrations.
Application Compartmentalization
While we had some success in transitioning a good deal of client code to extensible React components, we had still not achieved compartmentalization at the application level. Every web request was still ultimately backed by our monolithic web application servers, allowing degradations in unrelated parts of the application server to affect the performance of our web page. We began to sketch out a new type of application architecture for our Deposit SPA, a completely client-side site, backed by S3, and powered completely by RESTful API requests through our application gateway. This structure would move our application completely off of our legacy MVC web servers, allowing our initial application page load to be completely powered by S3. By utilizing separate buckets per environment, we also achieved the compartmentalization we desired.
We chose to leverage the knowledge we had gained from our component integration and settled again on client-side React + Redux. Given the importance of a payments page to any eCommerce business, we wanted to build an experience that was not only intuitive and modern, but performant. We decided we wanted to start from a new base HTML page, which would serve as the distribution channel for our new single page application. Unlike our initial integration, which leveraged existing web application servers and internal services to determine what HTML and Javascript to return on a page request, we chose to route all requests for our deposit page to return a static HTML file hosted in S3, which we generated at build time. This file contained all the markup and styling required to render our initial page, as well as the script to include our SPA code. Deploying a new release to production simply required us to copy the new Javascript code to S3 and overwrite the existing HTML file with our newly generated one. The updated HTML would be returned on subsequent requests, pulling in and executing our latest Javascript. At this point, our SPA would take over, making initial data-hydration calls to our API gateway and uplifting the remaining parts of the user experience.
In order to keep the experience snappy, we wanted to be able to cache as much of the client code as possible, while still maintaining the ability to release new code to our customers on demand. We had already decided to use Webpack for bundling our code and generating the base HTML for our page, so we were able to utilize build plugins to generate content hashing for our bundled files. We ended up with 2 types of build artifacts from this design:
- Content hashed Javascript assets that could be aggressively cached on clients and our CDNs
- A lightweight HTML file that allowed for straightforward deployment
Through this design, we were able to ensure that a new production deploy of our application would utilize the latest, content-hashed JS bundles referenced in the new HTML and that we could aggressively cache our JS code to improve future visit performance. Clients only had to download JS chunks that were new or had changed, saving unnecessary round-trips for returning customers.
With this new distribution strategy, the mechanics of shipping a new build of our code to production became even more straightforward than our initial component integration.
- New JS code is copied to S3. We content-hash each JS file to ensure we are able to set a long max-age cache-control header, reducing unnecessary network traffic on the client.
- New HTML is copied to S3, which references the JS code we just copied to S3.
- Subsequent requests to our web servers receive the new HTML
- Web clients fetch and execute the new code, pulling any new JS content from S3 and initializing our SPA.
From this point forward, our distribution mechanisms were in place and the development process looked very similar to our component based approach, even leveraging some generic components we had broken out into a component library. With React Router handling all of our client-side routing needs, we were able to quickly build and integrate this new single page application, dramatically improving one of our most critical user flows.
More Apps, More Products
Stay tuned for Part III of this series, where we will show how DraftKings Engineering has built upon these principles to create new Node.JS server-side applications for our market leading Online Sportsbook and Casino web products.