The Architecture Showdown That Nobody Asked For – But Everyone Needs
What happens when you drop four identical feature changes into six React storefronts built with different architectural philosophies? Chaos. Or, if you're smart about it, a clear lesson in what works and what doesn't. A recent deep-dive by a team of frontend engineers at a major e-commerce consultancy put exactly this question to the test. They took six React-based online store prototypes – each architected differently, ranging from Feature-Sliced Design (FSD) to Clean Architecture to a plain modular setup – and subjected them to the same four real-world modifications. The goal? Find out which architecture bends without breaking, and which one shatters under pressure.
If you've ever debated whether FSD is overkill for a simple store, or wondered if Clean Architecture actually delivers on its promise of maintainability, this article is for you. The results are surprising, non-obvious, and might just change how you think about structuring your next React project. Source
Why Architecture Matters (More Than Your Linter)
Before we dive into the six contenders, let's address the elephant in the room: why does frontend architecture matter for an e-commerce site? In theory, you could build a perfectly functional store with a single App.js file. But as soon as you need to add a new payment gateway, change the checkout flow, support a multi-currency display, or integrate a loyalty program – that monolithic file becomes a nightmare.
Modern e-commerce is a moving target. Products get new attributes, promotions change weekly, and user session logic evolves. The architecture you choose determines how much pain each of those changes inflicts. The team behind this experiment wanted to quantify that pain. They built six variations of the same e-commerce app – a typical storefront with product listings, a cart, checkout, and user profiles – and then applied four modifications:
- Add a new product attribute (e.g., "eco-friendly" badge with filtering)
- Change the checkout flow (split into two steps instead of one)
- Integrate a third-party loyalty API (fetch points and display discount)
- Migrate the cart state from Redux to Zustand (state management swap)
Each modification was implemented by the same developer (to control for skill variance), and metrics like time to implement, lines of code changed, and number of files touched were recorded.
The Six Contenders: From FSD to Free-for-All
The experiment covered six distinct architectural approaches. Let's meet them:
| Architecture | Core Idea | Typical File Count (starting) |
|---|---|---|
| Feature-Sliced Design (FSD) | Business domains sliced into features, each with its own UI, model, and API layers | ~45 |
| Clean Architecture (CA) | Dependency inversion with entities, use cases, and adapters | ~50 |
| Modular (feature folders) | Group by feature, but no strict layering | ~30 |
| Atomic Design with state separation | UI components by atomic level, state in separate stores | ~35 |
| Classic MVC (Redux-heavy) | Single store, actions, reducers, connected components | ~25 |
| Flat structure (no architecture) | Everything in components/ and utils/ |
~15 |
All apps used React 18, TypeScript, and the same UI library (Material UI). The state management was initially Redux Toolkit (except the flat version, which used local state).
Change #1: Adding a Product Attribute – The Easy Test
The first change was deliberately simple: add an eco_friendly boolean to product data, display a green badge on product cards, and allow filtering by it.
Results:
- Flat structure: 4 files changed, 15 minutes. The developer simply added a field to the product type, updated the card component, and slapped a filter toggle on the listing page. Quick and dirty.
- Modular: 5 files, 18 minutes. Slightly more because the feature folder had its own types and a small selector.
- Atomic Design: 6 files, 22 minutes. The badge component lived at a different atomic level than the card, requiring a prop pass-through.
- Clean Architecture: 11 files, 40 minutes. The entity layer needed a new field, the use case for filtering had to be updated, the adapter for the API had to map the new field, and the UI layer needed a new presenter. The dependency inversion paid off in theory but slowed down trivial changes.
- FSD: 9 files, 35 minutes. Similar to Clean but with less ceremony. The feature slice for "product card" got a new UI element and a model change, but the strict separation between api, model, and ui subdirectories forced touching multiple files within the same slice.
- MVC (Redux-heavy): 7 files, 25 minutes. The reducer needed a new case, the action creator, a selector, and the component. Not terrible, but the global store meant a small change rippled.
Key insight: For trivial changes, flat and modular win. Clean and FSD add overhead that feels wasteful. But the authors note that this is the "honeymoon phase" – the real test comes with complexity.
Change #2: Splitting the Checkout Flow – The Complexity Ramp
Checkout is the heart of any store. The modification: turn the single-step checkout (address + payment + review) into a two-step flow (address first, then payment + review on a second page).
Results:
- Flat structure: 12 files, 1 hour 20 minutes. The developer had to manually untangle the monolithic checkout component, create a new page, and manage state persistence across steps. The lack of structure made it easy to introduce bugs – the cart total disappeared on step two.
- Modular: 8 files, 50 minutes. The checkout feature folder already had some separation, so splitting into CheckoutStep1 and CheckoutStep2 was cleaner. State management still required careful wiring.
- Atomic Design: 10 files, 1 hour. The atomic components (input fields, buttons) were reusable, but the page-level orchestration was messy because there was no clear boundary for checkout logic.
- Clean Architecture: 7 files, 40 minutes. The use case for "process checkout" could be split into two smaller use cases without affecting other parts. The dependency inversion meant the UI could change without touching business logic. This was the first win for Clean.
- FSD: 6 files, 35 minutes. The checkout feature slice already had a clear separation between ui, model, and api. The developer could create a new checkout-step2 sub-slice and reuse the existing model for cart data. The strict boundaries prevented accidental coupling.
- MVC: 9 files, 55 minutes. The global store made sharing state between steps easy, but the actions and reducers grew in complexity. The developer had to be careful not to break other parts of the app that depended on the same reducer.
Key insight: As complexity increases, structured architectures (FSD, Clean) start to shine. The flat approach, which was fastest for the simple change, became a liability. The authors note that checkout is a bounded context – and architectures that respect boundaries (FSD especially) handled it best.
Change #3: Integrating a Loyalty API – The External Dependency
Third change: connect to a third-party loyalty service (e.g., a points system) and display the user's points and a discount on the cart page. This required an API call, error handling, and UI updates.
Results:
- Flat structure: 14 files, 1 hour 45 minutes. The API call was thrown into a utility file, state was added to the cart component, and error handling was bolted on. The result worked but introduced tight coupling between the cart UI and the loyalty service.
- Modular: 10 files, 1 hour 10 minutes. A new loyalty feature folder was created, which helped isolate the logic. However, the integration with the cart required cross-feature communication, which the modular approach didn't have a clear pattern for.
- Atomic Design: 13 files, 1 hour 30 minutes. Similar issues to modular, plus the atomic components had to be extended to accept new props for points display.
- Clean Architecture: 8 files, 50 minutes. The adapter pattern shone here. The developer created a LoyaltyApiAdapter and a LoyaltyUseCase. The cart presenter only needed to call the use case, and the UI remained unchanged. The dependency inversion meant swapping the loyalty provider (e.g., from points to cashback) would be trivial.
- FSD: 7 files, 45 minutes. The loyalty feature slice was created with its own api, model, and ui. Integration with the cart was done through the shared session model. The clear feature boundaries prevented the loyalty logic from leaking into the cart slice.
- MVC: 11 files, 1 hour 5 minutes. The global store was both a blessing and a curse. Adding new state was easy, but the developer had to ensure the new reducer didn't conflict. Error handling required extra actions and selectors.
Key insight: External integrations benefit from clean separation. Both Clean Architecture and FSD handled the adapter pattern well, but FSD's feature isolation felt more natural for a UI-heavy app. The authors point out that Clean Architecture's abstraction layers can feel over-engineered if the integration is simple, but for real-world loyalty APIs (which often have complex logic), it pays off.
Change #4: Swapping State Management – The Refactoring Gauntlet
The final change was the most invasive: migrate the cart state from Redux Toolkit to Zustand. This involved changing how the cart reads and writes data across the entire app.
Results:
- Flat structure: 22 files, 2 hours 30 minutes. The cart state was scattered across components, utility functions, and the Redux store. Finding every reference required manual searching. Two bugs emerged: a stale product count and a missing discount recalculation.
- Modular: 15 files, 1 hour 45 minutes. The cart feature folder contained most of the state logic, but some components outside the folder had direct useSelector calls. Refactoring required updating imports and hooks.
- Atomic Design: 18 files, 2 hours. The atomic components were mostly stateless, but the page-level containers had Redux dependencies. The lack of a clear state boundary made it hard to know where to stop.
- Clean Architecture: 10 files, 1 hour 10 minutes. The use cases and entities were completely decoupled from Redux. The developer only needed to change the adapter layer (the CartStoreAdapter) and the UI's store injection. Business logic was untouched.
- FSD: 9 files, 1 hour. The cart feature slice had a single model directory that contained the Redux store logic. Swapping to Zustand meant rewriting the model layer while keeping the ui and api layers unchanged. The boundaries were so clean that the developer didn't even need to touch product or checkout slices.
- MVC: 17 files, 2 hours 10 minutes. The global Redux store meant every action and reducer for the cart had to be migrated. Selectors were used across the app, requiring updates in many components.
Key insight: This is where the architectural investment truly paid off. FSD and Clean Architecture made a major refactoring feel like a minor task. The flat and MVC approaches turned it into a bug-ridden ordeal. The authors emphasize that in real e-commerce projects, state management swaps happen more often than you think – as libraries evolve, bundle sizes matter, and teams migrate to newer patterns.
The Verdict: Which Architecture Wins?
After four changes, the team compiled a composite score based on time, bug count, and maintainability. Here's the final ranking:
| Architecture | Total Time (all 4 changes) | Bugs Introduced | Maintainability Score (1-10) |
|---|---|---|---|
| Feature-Sliced Design (FSD) | 3 hours 55 min | 0 | 9 |
| Clean Architecture | 4 hours 25 min | 0 | 8 |
| Modular (feature folders) | 4 hours 50 min | 1 | 6 |
| Atomic Design | 5 hours 12 min | 2 | 5 |
| Classic MVC (Redux-heavy) | 5 hours 15 min | 2 | 4 |
| Flat structure | 6 hours 20 min | 4 | 2 |
FSD came out on top, but not by a landslide. The authors note that FSD's strength is its pragmatic balance – it provides enough structure to isolate changes without the overhead of Clean Architecture's multiple abstraction layers. For a React storefront, where UI is king and business logic is often simple CRUD, FSD's feature-first mindset feels natural.
Clean Architecture came second, and the authors acknowledge it might perform better in apps with complex business rules (e.g., inventory management, dynamic pricing). However, for a typical store, the extra layers felt like overkill.
The flat structure, despite its early win, collapsed under complexity. The authors wryly note that "no architecture" is fine until it's not – and by then, it's too late.
What This Means for Your Next Storefront
So, should you drop everything and rewrite your e-commerce app in FSD? Not necessarily. The experiment has limitations: it was done by a single developer on a greenfield project, and the changes were pre-defined. Real-world projects have legacy code, multiple teams, and unpredictable requirements.
But the lessons are clear:
-
Invest in boundaries early. Whether you choose FSD, Clean, or another pattern, the most important thing is to separate concerns. The projects that suffered most were those where cart logic bled into components, or where API calls were scattered.
-
State management abstraction is worth it. The ability to swap Redux for Zustand (or anything else) without rewriting the UI is a superpower. Both FSD and Clean provided that through clear model/use-case layers.
-
Don't over-abstract for simple apps. If your store has three pages and will never grow, flat is fine. But if you're building for the long term, the overhead of FSD or Clean pays for itself after the third or fourth change.
-
Feature isolation beats layer isolation. For React apps, the authors found that organizing by feature (FSD) was more intuitive than organizing by layer (Clean). Developers knew where to put new code and where to look for existing code.
The original article on Habr dives deeper into the code examples and includes specific directory structures for each architecture. Source
Final Thoughts
Architecture is not a religion. The best architecture is the one that your team understands and that fits your problem domain. But if you're building a React storefront that you expect to evolve – and let's be honest, every storefront evolves – then the evidence from this experiment suggests that Feature-Sliced Design offers the best balance of structure, flexibility, and real-world performance.
Clean Architecture is a close second, especially if your business logic is complex. And the flat structure? It's a great way to prototype. Just don't ship it to production unless you're ready for the pain.
The team behind this experiment plans to repeat it with a larger codebase and multiple developers. We'll be watching. In the meantime, the next time someone asks you "FSD, Clean, or modular?", you now have data to back up your answer.
This article is based on the original research published on Habr. Read the full breakdown at Source.
Comments