There's this weird disconnect in our industry. We debate architecture patterns endlessly about "clean code," and then... we ship something completely different. Today, I want to show you what I actually build - not the theoretical perfect architecture, but the one that ships, scales, and makes sense at 2 AM.
After building everything from cannabis e-commerce platforms to SaaS dashboards, here's what I've learned: the best architecture is the one that's obvious. Not clever, not over-engineered - just obvious.
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/
Dead simple. Each feature is self-contained. Shared stuff is actually shared. Server code stays on the server. No mystery folders, no clever abstractions.
Here's a real component from production - a blog card that taught me more about architecture than any book:
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>
)
})
Notice what's NOT there? No prop drilling from 5 levels up. No context provider spaghetti. No "clever" abstractions. Just a component that does one thing well.
After years of experimenting, here's the pattern that actually works:
// 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
}
// 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
import { BlogCard } from "@/features/blog/components/card";
import { Button } from "@/shared/components/ui/button";
import { api } from "@/server/trpc";
One glance and you know exactly where everything comes from. No detective work required.
Here's something interesting - I build a lot of hero sections. Every feature gets one. And they all follow 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>
)
})
Centered. Focused. No distractions. The architecture mirrors the UI.
I used to obsess over the "perfect" architecture. Domain-Driven Design, Clean Architecture, Hexagonal Architecture - I tried them all. Then I realized something: the best architecture is the one your team understands instantly.
My current approach isn't revolutionary. It's not going to get me conference talks. But you know what? New developers understand it in minutes. Features ship faster. Bugs are easier to track down.
Here's how I know an architecture works - the "3 AM test":
Can you find the bug at 3 AM? With feature folders, yes. The error is in the checkout feature? Check features/checkout
.
Can a junior dev add a feature? Create features/new-thing
, follow the pattern from other features. Done.
Can you delete a feature cleanly? Delete the folder. If anything breaks, it wasn't properly isolated.
I went through a phase where everything had to be a monorepo with 47 packages. Took 5 minutes just to understand the import paths. Now? One codebase, clear boundaries.
I once created a "FormBuilder" that could handle any form. It had 2000 lines of configuration options. Now I just write forms. Takes 10 minutes, works every time.
Spent weeks on a type system that covered every edge case. Nobody understood it. Now I type what matters, any what's pragmatic, and move on.
Here's 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 is right there. No hunting, no guessing, no "where did they put this?"
Architecture isn't just folders. It's the entire developer experience:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
}
}
Simple scripts. No custom build tools. No proprietary abstractions. Just the tools everyone knows.
Six months into using this architecture on multiple projects:
But the real payoff? I stopped thinking about architecture. It just works, gets out of the way, and lets me focus on building features users actually care about.
Good architecture is like good design - when it's right, you don't notice it. It doesn't scream "look how clever I am!" It quietly does its job and lets you do yours.
My architecture isn't innovative. It won't win any awards. But it ships, it scales, and most importantly - it makes sense. In a world of over-engineered solutions, sometimes the best answer is the simple one.
Stop chasing the perfect architecture. Build something clean, centered, and shareable. Your future self (and your team) will thank you.
What's your approach? Do you lean toward simplicity or do you prefer more structure? I'd love to hear what's actually working for you in production, But we don't have comments system in this blog. 555 (This mean lol in thai you know, I think you can google it)
Related Posts