The Architecture Dilemma Every Vue Developer Faces
You’ve been there: your Vue 3 project starts beautifully organized, but after a few sprints, finding the right component feels like searching for a needle in a haystack. Your components folder has 47 files, your utils folder is a graveyard of functions nobody understands, and that store directory? Let’s not even go there.
The question isn’t whether you need architectureβit’s which one. Today, we’re diving deep into two popular approaches: traditional Modular Design and the increasingly popular Feature-Sliced Design (FSD). Both promise scalability, but they take radically different paths to get there.
What is Modular Design?
Modular Design is the OG approach most Vue developers know and love. It’s all about organizing code by technical concernsβgrouping similar types of files together regardless of what feature they belong to.
The Classic Structure
src/
βββ components/
β βββ UserAvatar.vue
β βββ UserProfile.vue
β βββ ProductCard.vue
β βββ ProductList.vue
β βββ ShoppingCart.vue
βββ composables/
β βββ useAuth.ts
β βββ useProducts.ts
β βββ useCart.ts
βββ stores/
β βββ auth.ts
β βββ products.ts
β βββ cart.ts
βββ services/
β βββ api.ts
β βββ authService.ts
β βββ productService.ts
βββ utils/
β βββ formatters.ts
β βββ validators.ts
βββ views/
βββ HomePage.vue
βββ ProductPage.vue
βββ CheckoutPage.vue
The Modular Philosophy
“Group by what it is, not what it does.”
Modular Design follows the principle of separation by layer. Components live with components, stores with stores, and utilities with utilities. It’s intuitive for developers coming from traditional MVC backgrounds and feels natural when you’re building small to medium applications.
Strengths of Modular Design
1. Low Entry Barrier
New developers can jump in immediately. Need a component? Check the components folder. Need state management? Look in stores. The mental model is straightforward.
2. Easy Code Discovery
When everything of the same type lives together, you know exactly where to look. No guessing games about which feature folder contains the mixin you need.
3. Promotes Reusability
Since components aren’t tied to specific features, developers naturally think about making them reusable. Your Button.vue can be used anywhere.
4. Works Great for Small Teams
When you have 2-5 developers, everyone can keep the entire codebase in their head. The simpler structure reduces cognitive overhead.
The Dark Side of Modular Design
But here’s where things get messy as your app grows:
1. The Coupling Nightmare
Your UserProfile.vue imports from useAuth, which imports from authStore, which imports from authService, which imports utilities from three different files. Change one thing, and you’re playing whack-a-mole with bugs.
2. Feature Hunting
Want to understand how checkout works? Good luck. You’ll need to jump between CheckoutPage.vue, ShoppingCart.vue, useCart.ts, cart.ts in stores, and orderService.ts. The feature is scattered across the entire codebase.
3. Folder Explosion
Your components folder now has 200+ files. Half are generic, half are feature-specific, and nobody knows which is which. Finding anything requires Cmd+P and prayer.
4. Refactoring Hell
Removing a feature means hunting through every layer of your app. Miss one store import? Enjoy your runtime errors.
Enter Feature-Sliced Design
Feature-Sliced Design (FSD) flips the script entirely. Instead of organizing by technical layer, it organizes by business domain and scope.
The FSD Structure
src/
βββ app/
β βββ providers/
β βββ styles/
β βββ index.ts
βββ pages/
β βββ home/
β βββ product/
β βββ checkout/
βββ widgets/
β βββ header/
β βββ product-list/
β βββ shopping-cart/
βββ features/
β βββ auth/
β β βββ ui/
β β β βββ LoginForm.vue
β β β βββ LogoutButton.vue
β β βββ model/
β β β βββ store.ts
β β β βββ types.ts
β β βββ api/
β β βββ authApi.ts
β βββ add-to-cart/
β β βββ ui/
β β β βββ AddToCartButton.vue
β β βββ model/
β β β βββ store.ts
β β βββ api/
β βββ product-filter/
βββ entities/
β βββ user/
β β βββ ui/
β β β βββ UserAvatar.vue
β β β βββ UserCard.vue
β β βββ model/
β β β βββ store.ts
β β β βββ types.ts
β β βββ api/
β βββ product/
β βββ ui/
β β βββ ProductCard.vue
β βββ model/
β β βββ store.ts
β β βββ types.ts
β βββ api/
βββ shared/
βββ ui/
β βββ Button.vue
β βββ Input.vue
βββ lib/
β βββ utils.ts
βββ api/
The FSD Philosophy
“Group by scope and impact, enforce strict isolation.”
FSD introduces layers (app, pages, widgets, features, entities, shared) that represent different levels of abstraction. Each slice is self-contained with its own UI, business logic, and API layer.
The Core Principles
1. Layers Have Hierarchy
-
app: Global setup, providers, routing -
pages: Route-level components -
widgets: Large composite UI blocks -
features: User interactions (business features) -
entities: Business domain models -
shared: Generic reusable code
2. Import Rules (The Secret Sauce)
Code can only import from layers below it or the same layer. A feature can use entities and shared, but not other features. This prevents the tangled web of dependencies that plague modular designs.
3. Public API Pattern
Each slice exports only what’s needed through an index.ts. Internal implementation stays hidden. Change internals freely without breaking consumers.
Real Vue 3 Example: Add to Cart Feature
Modular Design approach:
<script setup lang="ts">
import { useCartStore } from '@/stores/cart'
import { useProducts } from '@/composables/useProducts'
import { trackEvent } from '@/utils/analytics'
import { showToast } from '@/utils/notifications'
const props = defineProps<{ productId: string }>()
const cartStore = useCartStore()
const { getProduct } = useProducts()
const addToCart = async () => {
const product = await getProduct(props.productId)
cartStore.addItem(product)
trackEvent('add_to_cart', { productId: props.productId })
showToast('Added to cart!')
}
script>
FSD approach:
features/
βββ add-to-cart/
βββ ui/
β βββ AddToCartButton.vue
βββ model/
β βββ useAddToCart.ts
β βββ types.ts
βββ api/
β βββ addToCartApi.ts
βββ index.ts
<script setup lang="ts">
import { useAddToCart } from '../model/useAddToCart'
import { Button } from '@/shared/ui'
const props = defineProps<{ productId: string }>()
const { addToCart, isLoading } = useAddToCart()
const handleClick = () => addToCart(props.productId)
script>
<template>
template>
// features/add-to-cart/model/useAddToCart.ts
import { ref } from 'vue'
import { useCartStore } from '@/entities/cart'
import { trackEvent } from '@/shared/lib/analytics'
import { showToast } from '@/shared/ui/toast'
export const useAddToCart = () => {
const isLoading = ref(false)
const cartStore = useCartStore()
const addToCart = async (productId: string) => {
isLoading.value = true
try {
await cartStore.addItem(productId)
trackEvent('add_to_cart', { productId })
showToast('Added to cart!')
} finally {
isLoading.value = false
}
}
return { addToCart, isLoading }
}
// features/add-to-cart/index.ts
export { default as AddToCartButton } from './ui/AddToCartButton.vue'
export { useAddToCart } from './model/useAddToCart'
The Showdown: Feature by Feature
1. Scalability
Modular Design: βββ
Works well up to ~50 components and ~20 routes. Beyond that, cognitive load skyrockets. You’ll spend more time navigating than coding.
Feature-Sliced Design: βββββ
Designed for scale. Add 100 features? No problem. Each feature is isolated. Your features folder might be huge, but each slice is small and focused.
2. Team Collaboration
Modular Design: βββ
Merge conflicts are common. Everyone touches the same folders. Two developers working on different features will both modify components/ and stores/.
Feature-Sliced Design: βββββ
Features are isolated. Team members rarely work in the same slice. Pull requests are cleaner. Code reviews focus on business logic, not file organization.
3. Learning Curve
Modular Design: βββββ
Junior developers get it immediately. It maps to familiar patterns. “Where’s the button component? In the components folder, duh.”
Feature-Sliced Design: βββ
Steeper curve. Developers need to understand layers, import rules, and the public API pattern. Takes 1-2 weeks to internalize. Documentation is crucial.
4. Code Reusability
Modular Design: ββββ
Easy to reuse components since they’re centralized. However, you’ll create “god components” that try to handle every use case.
Feature-Sliced Design: ββββ
Reusability is intentional, not accidental. shared contains truly generic code. entities handle domain models. Features stay specific. Less generic bloat.
5. Feature Removal
Modular Design: ββ
Delete a feature? Hope you find all the pieces. Check components, stores, composables, services, utils, and pray you didn’t break something else.
Feature-Sliced Design: βββββ
Delete the folder. That’s it. Thanks to strict isolation and public APIs, removing a feature is surgical. If it compiles, it works.
6. Testing
Modular Design: βββ
Testing is straightforward but often requires mocking multiple layers. Your component test needs to mock stores, services, and utilities.
Feature-Sliced Design: ββββ
Each slice is independently testable. Test the feature’s model layer in isolation. Test UI with a minimal setup. Less mocking, more confidence.
7. Refactoring
Modular Design: ββ
Risky. Change one composable, and you might break three features. Hard to know what’s safe to change.
Feature-Sliced Design: ββββ
Safe. Public APIs create clear contracts. Refactor internals freely. Only breaking the public API affects consumersβand your IDE will tell you exactly where.
When to Choose What?
Choose Modular Design When:
- Your team is small (2-5 developers)
- The project is small-medium (<50 components)
- Time to market is critical (FSD has setup overhead)
- Team is junior (lower learning curve)
- The product scope is unclear (premature abstraction is dangerous)
- You’re building a proof of concept or MVP
Choose Feature-Sliced Design When:
- Your team is growing (5+ developers)
- The project is large (100+ components)
- Long-term maintenance matters (planning for 2+ years)
- Multiple teams will touch the codebase
- Feature independence is important (microservices mindset)
- You need to onboard developers frequently
The Hybrid Approach (The Secret Third Option)
Here’s what nobody tells you: you don’t have to choose. Many successful Vue 3 projects use a hybrid:
src/
βββ features/ # FSD-style features
β βββ auth/
β βββ checkout/
βββ entities/ # FSD-style entities
β βββ user/
β βββ product/
βββ shared/ # FSD-style shared
β βββ ui/
β βββ lib/
βββ components/ # Modular-style global components
β βββ layouts/
β βββ common/
βββ composables/ # Modular-style global composables
βββ useMediaQuery.ts
Use FSD’s feature isolation for complex business logic, but keep a components folder for truly generic UI elements. Best of both worlds.
Migration Strategy: From Modular to FSD
Already have a modular project? Here’s how to migrate incrementally:
Phase 1: Create the structure
mkdir -p src/{features,entities,shared/{ui,lib}}
Phase 2: Move truly generic code
Move utilities and generic components to shared. This is low-risk.
Phase 3: Extract one feature
Pick a self-contained feature (authentication is often a good start). Move its components, store, and API to features/auth/. Update imports. Test thoroughly.
Phase 4: Repeat
Extract one feature per sprint. Don’t rush. The codebase will be hybrid for monthsβthat’s fine.
Phase 5: Extract entities
Once features are out, extract domain models to entities. This is the trickiest part.
The Verdict
There’s no universal winner. Modular Design and Feature-Sliced Design optimize for different things:
- Modular Design optimizes for simplicity and speed
- Feature-Sliced Design optimizes for scalability and maintainability
For most Vue 3 projects starting today, I’d recommend:
- Start modular (launch fast)
- Monitor complexity (watch for the pain points)
- Migrate to FSD when you hit 30-50 components or 5+ developers
The best architecture is the one your team can maintain. FSD is more powerful, but Modular Design is more forgiving. Choose based on where you are, not where you hope to be.
Resources
What’s your experience with project architecture in Vue 3? Have you tried FSD? Let’s discuss in the comments!
