The Art of Clean Feature Architecture: What I Actually Build
I organize code into feature-based folders, with minimal abstractions and obvious boundaries. This is the architecture I actually ship.
view .mdopen in claudeopen in chatgpt
There is a gap between what developers debate and what they ship. This article covers the architecture I actually build, not a theoretical version. It is the one that works at 2 AM.
My Real Architecture Philosophy
I have built e-commerce platforms and SaaS dashboards. Through that work, I settled on one principle: the best architecture is obvious, not clever.
Look at this structure from a recent project:
src/
├── features/
│ ├── blog/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api/
│ │ └── utils/
│ ├── chat/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── utils/
│ └── products/
├── shared/
│ ├── components/ui/
│ ├── hooks/
│ └── lib/
└── server/
Each feature stays self-contained. Shared code lives in shared/. Server code stays in server/. There are no mystery folders or abstractions that need a decoder ring to understand.
The Component That Changed My Perspective
A real component from production:
export const BlogCard = memo(function BlogCard({
article,
styles = DEFAULT_STYLES,
}: Readonly<Props>) {
const format = useFormatter();
const readTime = useMemo(
() =>
Math.ceil(
convertLexicalToPlaintext({ data: article.content }).split(" ").length /
200,
),
[article.content],
);
return (
<Link href={`/blog/${article.slug}`}>
<Card className="group h-full overflow-hidden">
{/*Clean, focused, single responsibility*/}
</Card>
</Link>
);
});
This component has no prop drilling, no context spaghetti, and no abstraction for its own sake. It has one job.
The Pattern I Keep Coming Back To
The pattern I use:
1. Features Own Their Domain
// features/chat/hooks/useChatApi.ts
export function useChatApi() {
// All chat logic lives here
// Not scattered across utils, helpers, services
}
// features/products/api/index.ts
export async function getProducts() {
// Product API calls stay with products
}
2. Shared Means Actually Shared
// shared/components/ui/button.tsx
// This button is used EVERYWHERE
// Not "might be shared someday"
// shared/hooks/useCopy.ts
// A hook that 5+ features actually use
// Not a "just in case" abstraction
3. Clean Imports Tell the Story
import { BlogCard } from "@/features/blog/components/card";
import { Button } from "@/shared/components/ui/button";
import { api } from "@/server/trpc";
One glance tells you exactly where everything comes from. This requires no detective work.
The Hero Component Philosophy
Every feature gets a hero section, and each one follows the same pattern:
export const HeroSection = memo(() => {
const t = useTranslations("pages.home.hero");
const [state, setState] = useState();
// Effects close to usage
// No effect chains
// No callback hell
return (
<section className="relative flex h-dvh items-center">
{/*Content*/}
</section>
);
});
This section stays centered and focused, with no distractions. The architecture mirrors the UI.
Why I Stopped Chasing Perfect
I tried Domain-Driven Design (DDD), Clean Architecture, and Hexagonal Architecture. The best one turned out to be whichever pattern your team understands without reading a slide deck first.
New developers pick it up in a day instead of a week. Features ship faster. Bugs land in places where they are obvious.
The Real-World Test
My test for whether an architecture works:
- Can you find the bug at 3 AM? With feature folders, yes. If the error sits in the checkout feature, you check
features/checkout. - Can a junior developer add a feature? They create
features/new-thingand follow the pattern from other features. - Can you delete a feature cleanly? You delete the folder. If anything breaks, the feature was not properly isolated.
The Mistakes That Led Me Here
The Monorepo Phase
I went through a phase where every project had to be a monorepo with 47 packages. Understanding the import paths alone took five minutes. Now I use one codebase with clear boundaries.
The Abstraction Addiction
I once built a FormBuilder that handled any form. It needed 2,000 lines of configuration options. Now I write forms directly. Each one takes ten minutes and works every time.
The Perfect Type System
I once spent weeks building a type system that covered every edge case. Nobody understood it. Now I type only what matters and move on.
What This Actually Looks Like in Production
Here is a real feature structure from a production app:
features/verification/
├── components/
│ ├── hero-section.tsx # The main hero
│ ├── verification-form.tsx # The form
│ └── results/ # Result states
│ ├── success.tsx
│ └── error.tsx
├── api/
│ └── index.ts # API calls
├── types/
│ └── index.ts # Types
└── utils/
└── validation.ts # Validation logic
Everything the verification feature needs sits right there. You never hunt for where things live.
The Tooling That Makes It Work
Architecture is not just folders. It is the entire developer experience:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
}
}
These are simple scripts. There are no custom build tools or proprietary abstractions, only the tools everyone already knows.
The Payoff
Six months into using this architecture on multiple projects:
- Onboarding time dropped to 1 day, down from 1 week.
- Feature development runs 40% faster. The team measured this and did not guess it.
- Bug resolution usually takes under an hour.
- Developer satisfaction improved noticeably.
The real payoff is this: I stopped thinking about architecture at all. Good design stays invisible. It just works.