Why this matters
A component is the smallest deployable and developable unit of a system — the JAR file in Java, the DLL in Windows, the Gem in Ruby, the npm package in Node. Components are the granularity level at which software is deployed: they can be linked together into a single executable, or kept as separately deployable plugins that connect at runtime.
The evolution toward runtime plug-ability is significant. When components can be swapped at runtime without recompiling the rest of the system, the architecture becomes a plugin architecture — new features, new integrations, and new behaviors arrive as new components, leaving the existing core untouched. A monolithic module that mixes unrelated concerns forces the entire system to be tested and redeployed whenever any single part of it changes. The goal of component architecture is to break that coupling so that each independently deployable piece has its own versioning and release cadence.
The problem
A monolithic module bundles unrelated concerns — changing any one part forces a full redeploy of everything else.
Bad
# user_auth.py — HTTP logic, DB models, email, AND auth rules all mixed together
class User:
id: int
email: str
def hash_password(pw: str) -> str: ...
def send_welcome_email(user: User) -> None: ... # depends on email server
def save_user(user: User) -> None: ... # depends on database
def validate_token(token: str) -> bool: ... # auth logic
def render_profile_page(user: User) -> str: ... # depends on HTML templates
# Changing the email template forces a full redeploy of auth logic, DB code, etc.
// src/app.ts — auth, DB, email, HTTP all in one module
export function hashPassword(pw: string): string { /* ... */ }
export function sendWelcomeEmail(user: User): void { /* email dep */ }
export function saveUser(user: User): Promise<void> { /* DB dep */ }
export function validateToken(token: string): boolean { /* ... */ }
export function renderProfilePage(user: User): string { /* template dep */ }
// A bug in sendWelcomeEmail forces a full redeploy of all auth and DB code.
The solution
Separate packages with clear boundaries — each with its own version and release cadence. A change to notifications never touches auth or user packages.
Good
# auth/ ← pure auth logic, no external deps
# tokens.py, passwords.py
#
# users/ ← user domain — depends on auth, not on email or HTTP
# models.py
#
# notifications/ ← email/SMS — versioned separately
# email.py
#
# api/ ← HTTP layer — depends on users + auth
# routes.py
# Each package has its own version tag.
# A change to notifications/email.py triggers only notifications tests + deploy.
// packages/auth/ — pure auth logic
// index.ts: export { hashPassword, validateToken }
//
// packages/users/ — user domain
// index.ts: export { User, saveUser }
//
// packages/notifications/ — email/SMS
// index.ts: export { sendWelcomeEmail }
//
// packages/api/ — HTTP layer
// index.ts: import from auth, users, notifications
// Fixing a bug in notifications only redeploys that package.
// auth and users packages are never touched.
Key takeaway
If you can't deploy one part of your system without redeploying everything, you have a monolith masquerading as components. True components have independent versioning and deployment — changing one part leaves the rest provably unchanged.