Learn Pinia - Scaling Store Architecture
Episode 19 of 23

Learn Pinia - Scaling Store Architecture

Large applications demand a clear store structure. This episode covers the per-feature stores/ folder structure, store, action, and getter naming conventions, how to split large stores, and code-splitting with dynamic store registration to keep the bundle lean.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Pinia makes small stores easy — and that's exactly where the scaling challenge lies: how do you organize dozens of stores without chaos? Folder structure, naming conventions, and code-splitting strategy determine whether your codebase grows healthily or falls into disarray.

Episode 19 covers store architecture at scale: per-feature folder structure, naming conventions, splitting bloated stores, and dynamic store registration for bundle size.

stores/ Folder Structure per Feature

The main principle: one stores/ folder with one file per domain. File names follow store names:

stores/ folder structure
src/stores/
  auth.ts
  cart.ts
  user.ts
  ui.ts
  index.ts

stores/auth.ts holds the auth store, stores/cart.ts holds the cart store, and so on. index.ts can re-export all stores so imports stay tidy:

JSRe-export in index.ts
export * from './auth'
export * from './cart'
export * from './user'
export * from './ui'

export * from './auth' lets components import from a single point. A flat structure like this is easier to navigate than piling up nested folders.

Naming Conventions

Consistent naming is the cheapest documentation:

  • Store: use<Name>Store — for example useCartStore.
  • Store id: the feature name — cart, auth, user.
  • Getter: a noun or value — totalPrice, isLoggedIn.
  • Action: a verb — addItem, checkout, fetchProfile.
JSConsistent naming conventions
export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] as CartItem[] }),
  getters: {
    totalPrice: (state) => ...,
  },
  actions: {
    addItem(item: CartItem) { ... },
  },
})

totalPrice (getter, a noun) and addItem (action, a verb) make the intent clear when read in a template. These conventions also make DevTools and logs easier to understand.

This consistency works like a shared language within a team: when a new developer reads useWishlistStore().addItem(product), they immediately know that the method modifies the wishlist without opening its definition. Conversely, a name like useStore().setData() forces readers to investigate what's actually being changed — a cognitive cost that looks small but accumulates in a large codebase. If you're unsure what to name something, pick a name that describes the end result of the operation, not how it works.

Splitting Bloated Stores

Signs a store is too large: more than one responsibility, or state that's never used together. The solution is to split it per domain:

JSBefore and after splitting
// Before: one store for everything
useShopStore -> { user, cart, wishlist, orders, reviews }
 
// After: small stores per domain
useUserStore()
useCartStore()
useWishlistStore()
useOrderStore()

Splitting large stores makes each part easier to test and less likely to trigger unnecessary re-renders. Coordination between stores can still be done with the composing patterns from episode 12.

A second sign worth watching for is a getter that depends on many fields from different domains. If a getter reads state.cart, state.user, and state.ui at once, the store is very likely holding three domains at the same time — and a change in one domain will trigger a getter recalculation that isn't actually related. Splitting it means each store's getters only monitor their own dependencies.

Code-Splitting and Dynamic Store Registration

Pinia activates a store the first time it's called. Take advantage of this for lazy-loading modules:

JSDynamic store registration
async function loadAdminStore() {
  const mod = await import('@/stores/admin')
  return mod.useAdminStore()
}

await import('@/stores/admin') makes the admin store only get downloaded when it's needed. With route-level lazy loading and stores that get split along with it, the main bundle stays lean for users who never touch certain features.

Tip

Measure first with a bundle analyzer before deciding on code-splitting. If a store is small and rarely used, the effect is small; focus on large modules and rarely visited routes.

Closing

Episode 19 equips you with architecture patterns for scale. You can now lay out a per-feature stores/ folder structure, apply consistent naming conventions, split bloated stores, and use dynamic imports for code-splitting.

Key takeaways:

  • One file per store in src/stores/, re-exported through an index.
  • Convention: use<Name>Store, noun getters, verb actions.
  • Split stores that have more than one responsibility.
  • Stores activate on first call — take advantage of lazy loading.
  • Dynamic imports let large feature stores get split along.
  • Measure the bundle before optimizing code-splitting.

In the next episode, episode 20, we'll discuss the latest stable features of Pinia v4 — type improvements, Vue 3.5+ compatibility, @pinia/nuxt 1.x integration, and the state of the ecosystem in 2026, including @pinia/testing 2.x and Pinia Colada, which is now stable.

Learn Pinia - Scaling Store Architecture | Learning Pinia