Fluent Commerce Logo
Community
No alt text provided

A Journey of Feature Parity

Published: 30-01-2025

10 min read

#Inspiration

Hi everyone. Welcome to one of the first blog posts on the Fluent blog site. I was told to write about something interesting so I thought why not talk about the very first project I worked on when I joined. So without further ado, let’s get started.

This is the story of a year-long journey to achieve a slightly unusual engineering objective.

The Beginning

When I first joined Fluent Commerce, the company was in a pretty interesting place.

A couple of years before, the company had made the decision to recreate its front-end web app. There were several factors leaning into this, but the biggest one was to put full customizability into the hands of users. Our product targeted business-to-consumer retailers who needed something to manage their flow of inventory, orders and products. But we were not the only people in this space. What makes us really unique is that we’ve designed our product as a 'platform-as-a-service' - a product that lets users design their own workflow and web app to cater for their own business case. This flexibility and versatility was the key point we were pushing to drive our product ahead of our competitors.

Unfortunately, our previous front-end web service didn’t really meet this promise. For something we had marketed as flexible, it had an incredible amount of hard-coded behavior. The previous engineering teams did try making a lot of it configurable via JSON, but there’s a real difference between converting an already complete solution versus using a system that was designed from scratch to be configurable.

Hence, it was decided that we would rebuild it from scratch. It would be a fully customizable platform unlike anything our previous front-end had encountered.

Fast forward a few years to when I joined the company, and we had a brand new front end. It had a ton of promise. Manifest-driven configurations, flexible components with different kinds of configurable properties. It had all the makings of a good flexible app platform.

However, there was just one problem.

Our existing customers refused to leave the older solution.

For new customers, we were continuously attracting bigger and larger customers onto the new platform. They clearly liked it. But our existing customers were quite reticent when it came to migrating onto the new platform.

Why was that?

Well, aside from a natural inclination against change, there was one reason that came up again and again. And that reason was the lack of feature parity.

In both solutions, we offered an out-of-the-box package that they could use to kickstart their processes. This was meant to be like one of those sample templates that people could use to build their own solutions on top of, or they could start from scratch if they wanted to. The older solution was far less flexible than the new one, but it could do more out-of-the-box. In the new solution, there was, of course, the ability to create your own custom components to imitate the old one. But that required effort. Why move when you could just stay on the old one with everything you already had?

From a technical perspective, we really wanted to get our existing customers off the old one. After all, having to maintain two different products that do roughly the same thing is far from ideal. So Fluent Commerce made a decision - we would improve our reference solution (called Fluent Store) to match everything the old one did.

And so we started a year-long journey of achieving feature parity with an older product.

The Task

The app our team worked on was a web app called Fluent Store. It was aimed at helping store assistants track orders coming in remotely - either click & collect orders where the customers came into the store to collect, or home delivery orders which were shipped directly to the buyer’s home.

The typical user session on Fluent Store looks like the following:

  • An order arrives for some products.
  • Fluent’s back-end works out the best store/location to pick each item from.
  • Store Assistants monitor Fluent Store for an order fulfillment request, to pick and pack items from their physical location.
  • A fulfillment request is sent to the selected store, they go to their shelves or storage and pick up the items.
  • The user packs the order, usually putting on some kind of packing slip to mark the parcel for collection.
  • [Click n Collect] The order is held in-store for the customer to collect. [Home Delivery] A courier is booked to collect and deliver the order.

When the feature parity project started, this general flow of functionality was already there. However, when compared to our legacy solution. There were some gaps in the user experience when compared to the legacy web app. Some of the major ones included:

  • Barcode Scanner Support
    • Most steps previously required a mouse to click a button to proceed or increment a counter on the page. It was cumbersome for a store associate to put down their items to get on their computer and go through each step and then resume their pick pack process.
  • The ability to put items into multiple parcels.
  • The ability to accept a returned/refunded item.
  • Indicators of new and incoming orders/fulfillments.

There were many other minor UX enhancements—missing links, missing images, automatic redirects, icons, notifications, and many others—but these were the main ones that required the most effort to fix.

Each of these points were essentially broken down into epics, which was given to the team. Due to the way it’s structured, it only took us around 1-2 months per ‘epic’ to complete. But since there was so many features to cover, it took us about a year until we got everything across.

Highlights

As we worked on this project, we found a couple of unique benefits to working on a feature parity project.

  • #1 The use cases and desired functionality had already been defined for us

Since we already had a working app to base our new functionality on, we were able to use Servicepoint as a reference to base our solutions on. What this meant is that our design and discovery phase was sped up immensely - we knew exactly what each new component should do and how it served the user. Not only was this useful in saving time, but it also helped us define each task much more clearly than usual, which really helped us write tests as well.

As a side benefit, since our fleshed-out discovery allowed us to project our timeline to a higher degree than usual, management really liked it. They mentioned how it allowed them to give feedback on parts of our timeline and improve our strategy before we even started working on it, which saved time and cost.

  • #2 It gave us the opportunity to review the UX of the old app and improve upon it

Since our task was so clearly defined, we were also able to take feedback on the old app and refine its UX to our liking. Our designer, in particular, was very keen to redesign the UI to look and feel much better to use. This is not something that can be done every day as a redesign is usually very costly, but moving over to a new app allowed this to happen.

  • #3 It gave us the opportunity to implement each individual component as a potentially reusable component in other parts of our platform.

This is a big one and potentially the very reason why the company was so keen on this project in the first place. Building each component as a re-usable modularized component just improves the platform overall as well, giving new tools to our partners, and increasing the amount of things that can be done right out the gate.

Lessons Learnt

It wasn’t all sunshine, though. If we retrospectively look back at what we achieved, there are a bunch of areas where we could’ve done better. In particular, we learned a few of these hard lessons during our year-long journey:

  • #1 We didn’t pay enough attention to the real user stories

The downside of having a reference solution—we were so focused on just re-implementing everything from Servicepoint, we didn’t really sit down to review the validity of the original implementation or the user stories it served.

In particular, this really bit us in the back with our pack-into-multiple-parcel solution. Servicepoint relied on existing endpoints that already accepted packing into multiple parcels. So we thought it would be okay to design the front-end to accept data for those endpoints. This ended up being a mistake because of several limitations in the endpoint we used. You couldn’t for example, decide which parcel to place each item into. You couldn’t tell how many items were in each parcel or even place an item into multiple parcels (relevant for particularly big items which are broken down into separate parcels.) All the UI could do was specify that an order had X number of parcels. Which just didn’t serve the real-world purpose of the pack step well enough.

  • #2 We need to be more conscious about backwards compatibility

This is more of a result of working on a versionless platform as a service rather than this specific initiative, but it’s still important to note.

Due to the fact that our software lives on the cloud, one of our marketable features is the fact that our app is versionless. This meant that all users were always on the most ‘up-to-date’ fluent platform. However, this also came with a significant challenge in our releases. We always had to be mindful of changing current behavior, as it would mean all users would immediately switch to that new behavior on the next release. As a rule, we learned to not change existing behaviour without good reason.

Take, for example, a small minor behavior in our wave pick component. In this step, you selected the number of items you picked from the store before you went to the next step to pack them into parcels. The component included a quantity selected with up and down arrows that incremented and decremented a counter. In Servicepoint, this counter started at the maximum number of items, whereas in store, this started at zero.

Before the project started, fluent had already received feedback that the Servicepoint method was much better, as it was the most common case that you were able to pick all items from a store. (Not picking up an item meant it was missing, broken, or not serviceable in some way.) Starting from zero was just a burden as it required more clicks.

So we thought, okay, we’ll just switch the counter to start at max to match parity. After all, this was a feature parity project right? Well, this turned out to be a mistake. Existing customers started complaining soon after release that their UX flow had changed. We quickly reverted it, but then other customers started complaining that we reverted it. This became a small incident over what was a relatively minor detail in the behavior. In the end, we chose to make it configurable via a JSON setting.

Here’s another more technical example - Fluent Store comes with an SDK that allows partners to implement their own components. In order to do this, we feed data through so that all components have access to it.

It turns out some clients were using the entire data object in a ‘useEffect' dependency array. In one of our tickets, we wrapped parts of the data in a proxy that was able to recursively inject translated strings into the object. We thought it was okay to do—after all, this data object came from us. However, because some partners had been using it as a dependency in their 'useEffect’, it caused an infinite re-render loop, crashing their plugin in production.

Some websites recommend stringifying complex objects to avoid this sort of issue. However, we have no control over the code written by partners. (Not to mention that stringifying objects with proxies causes their own set of problems, as we found out ourselves.) So all we can really do is be careful of any changes to data and objects that could be consumed by downstream code.

  • #3 Our ability to improve upon the original was limited by both the framework and our time

Unfortunately for our designer, many of his designs were simply not feasible in the time frame we were given, for various reasons.

One of those reasons was framework limitations. We were creating components to be used within a platform. This meant that we were also limited by what the platform could do and how it functioned.

Take for example its layout. By default, components were laid out left to right in a 12 column grid. Components could specify how many columns to take up, and once a row was filled out, further components were placed in a new grid.

This limited the structure of what a page could look like. For example, design was not able to place two cards on top of each other, laid out next to a table that matched the total height of those cards. They couldn’t align content from one component with content from another component.

Of course, since we could code our own components, we had the option of creating components within components, or layout components that laid components in a specific manner, similar to javascript’s grid or flexbox layouts, or java swing’s border, box, grid, flow, gridbag layouts. However, it costed time and effort to spec and create these layout components. After all, if we made these layouts, we would want to make them in a way that was generalizable and flexible, not for our specific use case. There were many layouts that design wanted that we had to say no to. (Eventually though, we did cave in and decided to create one - the column component that stacked subcomponents on each other). And this impacted the final design of our components.

On the bright side, it highlighted limitations in our platform that all developers would face.

But anyway, we gave design and product a roadmap involving a phase 2 which would involve making the necessary framework changes to get the desired UI/UX. But that leads into the final point…

  • #4 If we approach a component with the intention of redoing it ‘properly’ in phase 2, it is most likely not going to happen.

This was something we kind of knew was likely to happen going in, but it’s worth noting this down anyway. A non-trivial amount of effort was spent thinking up the ideal solution, only for it to go down the drain. This happened for both our pack step and our returns step too.

Who knows though, maybe in the future we’ll get to revisit these and revamp them to our liking.

Reaching the End of Our Journey

And that’s all I have to say regarding our feature parity project. Looking back, we really got a lot done, but it’s always important to reflect and learn how we can improve our future projects.

For other programmers out there, I hope this gave you some useful insights into what it’s like to work on a feature parity project. For everyone else, I hope you enjoyed an interesting story about our everyday work.

As this is one of the first blog posts to be published to the wild, any feedback is welcome. Was it too short? Too long? Too dry perhaps? Email our media team at <INSERT EMAIL HERE> and tell us your thoughts! If you’ve been through a very similar journey yourselves, we would love to hear about it too.

Christopher Tse

Christopher Tse

I am a software programmer with 5 years of experience of backend programming in java. I have experience in using languages such as java, c, python, perl, shell scripting and lua. I use AWS services such as EC2 and S3 for personal projects and am familiar with the cloud technology stack. Other interests include writing literature, playing music and worldbuilding.

Disclaimer: All information and resources found on community.fluentcommerce.com are based on the opinions of the author (unless otherwise noted). All information is intended to inspire and motivate well thought through technical decisions inline with Fluent Commerce recommended practices and principles.

Copyright © 2025 Fluent Commerce

Fluent Logo