Flex Comp: The Technical Build
Flex Comp, Shopify’s new compensation system, lets employees allocate total reward between base salary, Restricted Stock Units (RSUs), and Stock Options, with future features like Shop Cash planned. The system removes one-year equity cliffs and allows allocation changes several times per year. While the user interface is straightforward—drag a slider to adjust allocations—the engineering behind it required solving unique challenges around privacy, encryption, and rapid iteration.
Structuring the Build for Speed
The project followed product-thinking principles, applied internally. The team committed to moving fast, both to hit timing requirements like pay periods and allocation windows and to avoid the drag of Parkinson’s Law. A detailed, prioritized backlog of isolated deliverables was established early. Teams combined front‑end and back‑end developers with specific infrastructure knowledge, ensuring expertise in areas like code indexing and internal search tools was well used.
API contracts between front end and back end were defined from the first week. As requirements solidified, contracts were extended before any back end changes, allowing the front end to develop independently and stay agnostic to early back end evolution.
Privacy and Access Control
Given the sensitivity of compensation data, privacy controls were a priority. The project was built in a private repository. To manage GitHub permissions granularly, the team created a dedicated GitHub organization for Flex Comp, then integrated the repository into the Shopify organization to leverage internal tooling for seamless deploys. Within Shopify’s org, users are auto‑added to the Shopifolk team, granting broad repo access. For Flex Comp, only core contributors added to a specific GitHub team could access the project, enabling tighter access control while retaining the benefits of internal tooling connected to the organization.
Encrypting Event Payloads
Bringing Flex Comp data into the existing data pipeline required protecting event payloads for product analytics and future studies on allocation behavior. The team evaluated alternatives, including a separate analytics platform instance, but time constraints ruled that out. Instead, they decided to encrypt the entire Kafka event payload before sending. The data team generated a 4096‑bit RSA key pair, sharing the public key with app teams to encrypt payloads while keeping the private key for decryption at the data warehouse.
Asymmetric encryption was chosen over symmetric because it allows any team to share the public key for sending encrypted payloads, while only the data team can decrypt. For events with structured payloads too large for the 4096‑bit key, a larger 6144‑bit RSA key was generated and used specifically for those events. The 6144‑bit key was not used universally due to its higher decrypting performance cost at the data warehouse; choosing key size per payload optimized that overhead.
Looking back, the team notes a better approach would combine symmetric and asymmetric encryption. At the application level, larger payloads could be symmetrically encrypted, with the symmetric key itself asymmetrically encrypted and sent alongside—similar to how SSL works. This avoids payload size constraints and performance issues.
Timestamping and Resiliency
An important detail was adding an explicit timestamp field within event payloads rather than relying on the Kafka event emission timestamp. This encodes more accurate domain‑level event timing. Data teams used the payload timestamp for metrics, and including it gives extra resiliency: events can be resent without affecting time‑based analytics.
Building the Front End to MVP
While legal considerations and back end changes were in flux, the front end moved forward to reach an MVP quickly and gather actionable feedback. Early constraints were:
- Data integrity is critical—what employees see must be correct.
- Sliders are the primary interaction for changing allocations.
- The interface must be accessible and work on mobile browsers.
- Regional regulatory differences will exist, though specifics were unknown.
- Employment type will introduce variation, also undefined initially.
- Multiple languages, locales, and currencies are required.
- The system is V1 but must be extensible.
- Answers to these questions would need to be built and integrated concurrently.
To handle likely pivots and still show correct information across regions and roles, a few guiding principles shaped early decisions on stack, backlog definition, and code structure:
- Optimize for parallelization. Expanding the work surface let more developers contribute early and sped progress.
- Single source of truth for data and calculations. With many contributors and evolving requirements, duplicating key data or calculations risked showing incorrect information. One shared source avoided that.
- Optimize for iteration. A single source of truth allowed an abstraction around calculation complexity, enabling plug‑and‑play data across the UI. Contracts kept interfaces stable, so dummy data could be returned and event handlers stubbed while business logic evolved independently. Tailwind was chosen for rapid, intuitive layout iteration.
The front end itself was relatively simple; the real work was getting it to a functional state that accounted for regional and employment‐type differences while leaving room for future features.
Front-End State and Data Flow
On the client side, the team had to manage two distinct types of state: API data about the user coming from the back end, and the live form state representing the employee’s allocation choices.
API State
The required queries shifted frequently during development as new regional and employee-type requirements emerged. The data itself was mostly static during a user session — things like employee name and role — so it only needed to be fetched on page load. Since this information was used across the entire page (for instance, to derive the salary compensation label), the team wrapped the query results in a React Context provider at the top level of the application.
To isolate the UI from changing data needs, all queries were assembled into a single provider. This gave the front end a stable contract to build against, similar to the one used with the back end team, and kept all data-shape updates confined to one file. The team deliberately chose a simple approach: the provider runs every query on mount by calling hooks extracted for each query, then stitches together the results along with any computed defaults or fallbacks.
Knowing this design might cause unnecessary queries or waterfall effects on routes that don’t need certain data, the team accepted that trade-off. Given the small team and a short timeline for V1, the naive solution let them focus on shipping features rather than optimizing data fetching prematurely.
Form State
The other state concern was the allocation form, driven by sliders and used to generate the summary table shown during confirmation steps. Because the form had many fields that interacted on every change (for example, increasing base salary reduces the equity pool available for options and RSUs, and resetting the form must update everything), the team used useReducer with action types defined as TypeScript union types.
There were initial concerns about whether the reducer pattern added unnecessary complexity — especially if the final product only needed a few event handlers — and about maintainability of dispatch calls spread throughout the component tree. The team solved this by abstracting both the state and the dispatch logic behind a custom hook with a clearly defined interface.
TypeScript was a key part of making this work. Type-safe action definitions and hook return types gave the front end a stable contract for the business logic and back-end data. That contract allowed two developers to build the form UI and the business logic in parallel using mocked values, and to keep mocks current as the API evolved. The mocks also made it possible to simulate artificial delays to test the page under different network conditions.
Parallelizing the Build
The contracts and abstractions were decisive for hitting the project’s ambitious timeline. The team’s goal was a fully functional end-to-end integration of the back end with an intentionally rough front end around the halfway point, leaving the second half for polish and edge cases. Having clear interface boundaries allowed developers to work concurrently on authentication, translation integration, form logic, slider UI, and the app frame — across time zones in North America and Europe.
Equally important was frequent, deliberate communication. The team already embraced asynchronous collaboration as part of Shopify’s Digital by Design culture, which reduced friction when jumping between workstreams.
Looking Past V1
With V1 shipped internally, the team is now focused on what comes next. Shipping this quickly required deliberate trade-offs, and the team kept notes on any technical or design debt they encountered, along with context from prototyping and exploration, to support future work.
Technical debt, used intentionally, can be one of the highest-leverage tools available — saying no to the wrong optimization at the wrong time is often the difference between finishing and not. The team is proud of the first release and sees it as an early step toward aligning rewards with the company’s mission rather than the market.



