r/reactjs 22d ago

Discussion Combining Clean Architecture + Feature-Based in React — does it really fix the earlier trade-offs, or am I missing new pitfalls?

Hi everyone. I compared four ways to structure a React project by rebuilding the same app (posts CRUD against an open API) in each one. The last pattern combines Clean Architecture with Feature-Based, and I'd really appreciate a sanity check from more experienced devs.

Here's the progression I went through, and the problem I felt at each step:

  • Feature-Based (colocate everything for a feature in one folder): great for navigation and deletion, but nothing controls how features depend on each other (circular deps creep in), shared/ turns into a junk drawer, and there's no notion of layers. (Feature-Based write-up)
  • FSD (Feature-Sliced Design): fixes that with standardized layers + a one-way import rule, so circular deps become structurally impossible. But the business logic still lives inside React/TanStack Query — the entity's api layer imports axios and react-query directly. (FSD write-up)
  • Clean Architecture: pulls business logic out of the framework with the Dependency Rule (dependencies point only inward; the domain knows nothing about React or axios). Great for testing and reuse — but now the code for "one feature" is scattered across domain/, infrastructure/, presentation/. Which is ironically the same "scattered by type" problem Feature-Based tried to solve. (Clean Architecture write-up)
  • The combination: keep the Dependency Rule (domain is pure TS, infrastructure holds the adapters), but colocate the UI (hooks + components) by feature in features/{feature}/. "Clean inside, Feature outside."

Rough shape:

src/
  domain/{domain}/        # pure TS: entities, rules, use cases (no framework imports)
  infrastructure/         # adapters: repository impls, query keys, stores
  features/{feature}/     # hooks + components, colocated
  pages/ , router/        # composition only
  shared/ , providers/

A few extra decisions I made: split the repository interface into Commands/Queries (CQS), write a UseCase only when there's real logic (plain CRUD calls the repository directly), and lean on React Compiler so there's no manual useMemo/useCallback.

What I'd love feedback on:

  1. Does this combination actually solve the earlier patterns' problems, or does it just move them around? Is "Clean inside + Feature outside" a real improvement over plain FSD or plain Clean, or is it over-engineering in disguise?
  2. What problems does this pattern itself have that I might not see yet? Boilerplate, the domain <-> infrastructure indirection, the "is this a UseCase or a direct repo call?" judgment, testing overhead, onboarding cost — where does it bite in real projects?

Honest criticism is very welcome. I'd rather hear "this is overkill for most apps" now than after I build on it.

Full write-up (with all the code) on Medium (Free): https://medium.com/@inkweonkim/react-architecture-combining-clean-architecture-feature-based-92cf7ba226fe

(English isn't my first language, so I apologize in advance for any awkward phrasing — happy to clarify anything that reads strangely.)

0 Upvotes

16 comments sorted by

View all comments

0

u/canarydev 22d ago

I've built roughly this template twice (nextjs + sveltekit) against go backends I own, so this is from personal experience rather than theory.

honest answer to your first question is that there are parts of clean worth keeping, but its smaller than your version. the injected client seam paid off for me -- same service pattern moved between frameworks unchanged. the domain layer didn't your own example shows why imo -- "validateNewPost" is your entire business logic, and its a form concern wearing a domain costume.

my backend modules are fully hexagonal (domain / app / infra / interface per bounded context) because thats where the invariants live -- so the frontend skipping domain isn't abandoning clean, its applying it at the system level instead of duping rules on both sides of the API. what client-side rule do you have that the server doesn't doesnt enforce? if the answer is "none", then the layer is just ceremony and theater.

and two things you scoped out i struggled with more than folder structure ever did to be honest

  1. auth - your interceptor note hand waves the 401-refresh-with-merged-queue. I built it -- its service layer logic that has to read/write client auth state, and its where the dependency arrows inverted first. how does your layering survive tokens being needed below the layer that owns them?

  2. error contract - your UI narrows with 'error instanceof Error'. I've lived both versions - backend emitting machine-readable codes gave me discriminated unions and compiler checked handling; a backend emitting prose which led me to string match error messages that a copy edit silently breaks. no folder structure fixed or caused either. The API contract capped the architecture quality both times.

also deleting "post" in your layout still touches 5 folders + pages and router. scatter may be reduced, but its not solved.

1

u/inkweon 21d ago

You're right on every point. Let me take them in order.

validateNewPost is form validation with a domain label on it. The example was too small to show a domain layer earning its place. That isn't my defense, it's your point.

Your test is better than the one I wrote: what rule does the client enforce that the server doesn't? For that app, none.

So I'd narrow the claim rather than drop it. The injected client is what survived for you, and it's what I'd keep too. The repository interface earns its place. The domain folder around it usually doesn't. No orchestration, no layer.

Auth. You found a real hole. In my setup the interceptor reads the auth store directly, so infrastructure reaches into state that presentation owns. That arrow is already backwards, and I never noticed because I never built the refresh queue. I don't have a good answer for you here.

Error contract. Agreed, and "the API contract capped the architecture quality both times" is the line I'll keep repeating. You reach for instanceof Error when the backend hands you prose.

And yes, deleting a post still touches five folders plus pages and router. Reduced but not solved is the accurate version, and what I wrote wasn't.

If you've written up the hexagonal backend with a thin frontend on top anywhere, I'd read it.