Suggest an editImprove this articleRefine the answer for “Modular architecture”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Modular architecture** divides an application into self-contained units that communicate only through defined public interfaces, hiding all internal implementation from outside code. ```javascript // Only this is visible outside the module: export { UserProfile } from './components/UserProfile.js'; export { useUser } from './hooks/useUser.js'; // userModule/internal/store.js stays completely private ``` **Key:** if changing a module's internal files breaks code outside it, the architecture is not truly modular.Shown above the full answer for quick recall.Answer (EN)Image**Modular architecture** divides an application into independent, self-contained units (modules) that communicate only through defined public interfaces, so teams can develop, test, and scale each piece without breaking the others. ## Theory ### TL;DR - Think of it like a restaurant kitchen: each station (prep, grill, sauce) owns its tools, handles its job fully, and passes finished work through a pass window (public API), never reaching into another station's storage. - The core principle: modules are black boxes. External code only sees what's explicitly exported. - Modules can depend on each other, but only through public `index.js` exports, never through internal file paths. - Decision rule: if you have multiple teams, features with independent lifecycles, or a codebase growing past 10k lines, modular architecture pays off. ### Quick example ```javascript // userModule/index.js (PUBLIC API - the only door in) export { UserProfile } from './components/UserProfile.js'; export { useUser } from './hooks/useUser.js'; export { fetchUser } from './api/userService.js'; // App.js (external code) import { UserProfile, useUser } from './userModule/index.js'; // OK: using public API import { cacheUser } from './userModule/internal/store.js'; // Wrong: reaching into internals directly ``` Everything inside `userModule/internal/` is the module's private business. Other modules have no idea it exists. ### Key difference Modular architecture isn't just organizing files into folders. The real test is whether changing one module's internal file structure breaks other modules. If it does, you have organized code, not modular code. The difference is **hard boundaries**: modules interact only through explicit contracts, so you can rewrite internals, swap implementations, or delete a module entirely without touching code outside it. ### When to use - Multiple teams on the same codebase, where each team owns a module and merges don't conflict - Features with independent lifecycles, so payments can update without touching auth - Codebase scaling beyond one developer, to prevent "everything depends on everything" - Plugin-like features that can be enabled or disabled at will - Code you want to reuse across web and mobile apps ### How bundlers handle module boundaries When Webpack or Vite processes your code, it traces imports from entry points and builds a dependency graph. Each module gets its own scope, so variables inside don't leak globally. Tree-shaking then strips out anything not exported through the public API. At runtime, attempting to import from `userModule/internal/store.js` fails if that path isn't exposed in `index.js`. The boundary isn't just a convention. It's enforced. ### Common mistakes **1. Importing from internal folders** ```javascript // Wrong import { userCache } from './userModule/internal/store.js'; // Right import { getUserData } from './userModule/index.js'; ``` When the module owner refactors internals, your code breaks. The public API is the contract. The internal structure is not. **2. Shortcuts through internals** ```javascript // paymentModule/components/PaymentForm.js // Wrong: bypassing public API to "save time" import { validateToken } from '../../authModule/internal/tokenValidator.js'; // Right import { validateToken } from '../../authModule/index.js'; ``` This creates hidden dependencies. Code reviewers see paymentModule and authModule as separate. In production, a refactor in authModule silently breaks PaymentForm. **3. Circular dependencies** ```javascript // userModule/api/user.js import { logActivity } from '../../analyticsModule/index.js'; // analyticsModule/api/analytics.js import { getUser } from '../../userModule/index.js'; // Wrong ``` Bundlers can't resolve circular imports cleanly. You get undefined exports or build failures. Modules should form a directed acyclic graph (DAG). If two modules both need something, extract it to a third shared module. **4. Exporting everything "to be safe"** ```javascript // Wrong: exposes internal helpers export { getUser } from './api/user.js'; export { validateEmail } from './internal/validators.js'; // Internal! export { userCache } from './internal/store.js'; // Internal! // Right: only what's intentionally public export { getUser } from './api/user.js'; ``` Once something is exported, external code starts depending on it. You can't change it without breaking consumers. Treat your `index.js` like a public API contract. **5. Shared config as a shortcut around duplication** ```javascript // Module A exports config export const config = { apiUrl: 'https://api.example.com' }; // Module B imports it import { config } from './moduleA/index.js'; // Module B now breaks if Module A changes the config shape // Better: each module manages its own config // moduleA/config.js and moduleB/config.js both define their own apiUrl ``` Duplication between modules is better than hidden coupling. ### Real-world usage - **React**: feature modules (auth, dashboard, settings) each export components, hooks, and services through `index.js` - **Express**: route modules (users, payments, admin) export routers, combined in `app.js` - **Redux**: feature slices (user, cart, notifications) are independent reducers with their own actions and selectors - **Next.js**: App Router treats each route as a self-contained module with its own layout and data fetching - **npm packages**: every package is a module you consume through its public API, never through internal source paths One pattern that causes the most grief: teams treating shared TypeScript types as a reason to import from internals. "It's just a type, it won't break anything" until the module owner moves that type and the build breaks across six other modules at once. ### Follow-up questions **Q:** How do you prevent circular dependencies between modules? **A:** Use a dependency graph tool like Madge or Dpdm in CI to catch them before merge. Design dependencies to flow in one direction: UI modules depend on feature modules, feature modules depend on core/shared, never the reverse. If two modules need to share logic, extract it to a third module both can depend on. **Q:** What's the difference between modular architecture and microservices? **A:** Modular architecture lives within a single codebase and process, modules share memory and run together. Microservices are separate processes that communicate over a network. Modular is easier to start with. Microservices add deployment complexity but allow independent scaling and deployment. **Q:** How do you handle shared state between modules without coupling them? **A:** Use a state management layer (Redux, Zustand, or Context) that no module owns outright. Modules dispatch actions and subscribe to state slices. Or use an event emitter: modules emit events, others listen, without knowing who's listening. Avoid direct imports of state objects between modules. **Q:** Can modules depend on each other directly, or should everything go through a central hub? **A:** Direct dependencies are fine as long as they're acyclic. A depends on B, B depends on C works fine. A depends on B, B depends on A doesn't. A central hub like a Redux store or event bus works but creates a bottleneck where every module must know about it. **Q (senior level):** How would you migrate a codebase where everything imports from everything into a modular structure without breaking production? **A:** Start by identifying natural domain boundaries: auth, payments, users, notifications. Create `index.js` files that define public APIs without changing any internal code yet. Then gradually migrate imports from internal paths to public APIs. Add a linter rule (eslint-plugin-import or similar) that forbids imports from non-index paths. Run this alongside regular feature work, not as a big-bang refactor. ## Examples ### Basic module structure in React ```javascript // userModule/index.js - public API, the only file outsiders import export { UserProfile } from './components/UserProfile.js'; export { useUser } from './hooks/useUser.js'; export { fetchUser } from './api/userService.js'; // userModule/internal/store.js - private, never exported from index.js const userCache = new Map(); export function cacheUser(id, data) { userCache.set(id, data); } // userModule/api/userService.js - private, used internally only import { cacheUser } from '../internal/store.js'; export async function fetchUser(id) { const response = await fetch('/api/users/' + id); const data = await response.json(); cacheUser(id, data); // caching is an internal detail return data; } // App.js - external code import { UserProfile, useUser } from './userModule/index.js'; // fetchUser is available too. cacheUser and userCache are not. ``` The caching strategy is an internal detail. If the team replaces the `Map` with a Redis client, zero external files change. ### Preventing circular dependencies ```javascript // paymentModule/index.js export { PaymentForm } from './components/PaymentForm.js'; // paymentModule/components/PaymentForm.js import { notifyUser } from '../../notificationModule/index.js'; // OK: importing from another module's public API // notificationModule/api/notify.js // Wrong - creates circular dependency back to paymentModule: // import { PaymentForm } from '../../paymentModule/index.js'; // Right: notificationModule knows nothing about paymentModule export function notifyUser(message, type) { console.log('[' + type + '] ' + message); } // Payment depends on notifications. Notifications don't depend on payment. // Each module can be tested in isolation. ``` Neither module can accidentally break the other's build. That's the point. ### Incremental migration to module boundaries ```javascript // Before: internal imports scattered across the app import { validateToken } from './authModule/internal/tokenValidator.js'; import { userCache } from './userModule/internal/store.js'; // Step 1: create public APIs without moving any files // authModule/index.js export { validateToken } from './internal/tokenValidator.js'; // userModule/index.js export { getUserFromCache } from './internal/store.js'; // Step 2: update call sites to use public APIs import { validateToken } from './authModule/index.js'; import { getUserFromCache } from './userModule/index.js'; // Step 3: add linter rule to block internal imports // .eslintrc: no direct imports from */internal/* paths allowed ``` Each step is small and independently testable. No need for a dedicated refactor sprint.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.