Conway's Law in Frontend Architecture
Conway's Law in Frontend Architecture
"Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations." — Melvin Conway, 1967
Your folder structure is a fossil record of your org chart. The boundaries between modules mirror the boundaries between teams. The APIs between components reflect the communication channels between humans. This isn't a failure of architecture—it's physics.
Understanding Conway's Law doesn't just explain why your codebase looks the way it does. It gives you a lever: change the team structure, and the architecture follows. Or, design the architecture you want, and restructure teams to match.
The Law in Action
┌─────────────────────────────────────────────────────────────────────┐
│ CONWAY'S LAW ILLUSTRATED │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ORGANIZATION STRUCTURE │
│ ══════════════════════ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Platform │ │ Product │ │ Growth │ │
│ │ Team │◄──►│ Team │◄──►│ Team │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ │
│ CODEBASE STRUCTURE (mirrors above) │
│ ══════════════════════════════════ │
│ │
│ src/ │
│ ├── platform/ ← Platform Team owns this │
│ │ ├── design-system/ │
│ │ ├── auth/ │
│ │ └── analytics/ │
│ │ │
│ ├── product/ ← Product Team owns this │
│ │ ├── dashboard/ │
│ │ ├── settings/ │
│ │ └── billing/ │
│ │ │
│ └── growth/ ← Growth Team owns this │
│ ├── onboarding/ │
│ ├── referrals/ │
│ └── experiments/ │
│ │
│ THE INTERFACES BETWEEN FOLDERS = THE APIS BETWEEN TEAMS │
│ │
│ • Platform exports components, hooks, utilities │
│ • Product consumes platform, exposes feature flags to Growth │
│ • Growth wraps Product features in experiments │
│ │
└─────────────────────────────────────────────────────────────────────┘
Why Conway's Law Is Inevitable
Communication Cost Drives Boundaries
┌─────────────────────────────────────────────────────────────────────┐
│ COMMUNICATION COST MODEL │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ WITHIN TEAM (Low friction) │
│ ══════════════════════════ │
│ • Same standup, same Slack channel │
│ • Shared context, shared vocabulary │
│ • Can tap shoulder, pair program │
│ • Code changes: immediate, informal │
│ │
│ Result: Tight coupling is cheap │
│ → Functions call functions directly │
│ → Shared state is acceptable │
│ → Implementation details leak across modules │
│ │
│ ACROSS TEAMS (High friction) │
│ ════════════════════════════ │
│ • Different standups, async communication │
│ • Different priorities, different roadmaps │
│ • Must schedule meetings, write documentation │
│ • Code changes: PRs, reviews, coordination │
│ │
│ Result: Loose coupling is necessary │
│ → Stable interfaces, versioned APIs │
│ → Contracts documented, changes negotiated │
│ → Implementation hidden behind abstractions │
│ │
│ THE BOUNDARY EMERGES NATURALLY │
│ ══════════════════════════════ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Team A │ │ Team B │ │
│ │ │ │ │ │
│ │ ┌───┐ ┌───┐ │ API/ │ ┌───┐ ┌───┐ │ │
│ │ │ a │──│ b │ │◄─Contract─►│ │ x │──│ y │ │ │
│ │ └───┘ └───┘ │ │ └───┘ └───┘ │ │
│ │ ╲╱ │ │ ╲╱ │ │
│ │ tight │ │ tight │ │
│ │ coupling │ │ coupling │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Modules within a team's ownership = tightly coupled │
│ Modules across team boundaries = loosely coupled │
│ │
└─────────────────────────────────────────────────────────────────────┘
Real-World Evidence
// SCENARIO 1: Single team, single page
// ════════════════════════════════════
// Team owns the whole feature, so they optimize for their convenience
// dashboard/
// ├── Dashboard.tsx ← Page component
// ├── widgets/
// │ ├── RevenueChart.tsx ← Direct imports of shared state
// │ ├── UserTable.tsx ← Calls dashboard APIs directly
// │ └── ActivityFeed.tsx ← Knows about dashboard context
// └── dashboard.store.ts ← Shared state for all widgets
// Widgets are tightly coupled to dashboard context
// This is FINE—same team, easy to coordinate changes
// SCENARIO 2: Platform team provides, Product team consumes
// ════════════════════════════════════════════════════════════
// Platform team's code (they control the interface)
// packages/design-system/
// ├── Button/
// │ ├── Button.tsx
// │ ├── Button.types.ts ← Explicit, documented types
// │ └── index.ts ← Controlled exports
// └── index.ts ← Public API surface
// Product team's code (they consume the interface)
// apps/dashboard/
// └── features/
// └── billing/
// └── PaymentForm.tsx
// import { Button } from '@company/design-system';
// // Only uses public API, no internal imports
// The interface is stable because changes require cross-team coordination
// Implementation can change without breaking consumers
Common Organizational Patterns and Their Architectures
Pattern 1: Feature Teams
┌─────────────────────────────────────────────────────────────────────┐
│ FEATURE TEAMS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Organization: │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Checkout │ │ Inventory │ │ Customer │ │
│ │ Team │ │ Team │ │ Team │ │
│ │ (FE+BE+PM) │ │ (FE+BE+PM) │ │ (FE+BE+PM) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ Resulting Architecture: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Monorepo or Multi-repo by feature │ │
│ │ │ │
│ │ apps/ │ │
│ │ ├── checkout/ ← Full vertical slice │ │
│ │ │ ├── frontend/ │ │
│ │ │ ├── backend/ │ │
│ │ │ └── shared/ │ │
│ │ │ │ │
│ │ ├── inventory/ ← Full vertical slice │ │
│ │ │ ├── frontend/ │ │
│ │ │ ├── backend/ │ │
│ │ │ └── shared/ │ │
│ │ │ │ │
│ │ └── customer/ ← Full vertical slice │ │
│ │ ├── frontend/ │ │
│ │ ├── backend/ │ │
│ │ └── shared/ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Characteristics: │
│ ✓ Teams can ship independently │
│ ✓ Clear ownership boundaries │
│ ✓ Low cross-team coordination │
│ ✗ Duplicated infrastructure code │
│ ✗ Inconsistent patterns across features │
│ ✗ Shared components become political │
│ │
└─────────────────────────────────────────────────────────────────────┘
Pattern 2: Platform + Product Teams
┌─────────────────────────────────────────────────────────────────────┐
│ PLATFORM + PRODUCT │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Organization: │
│ ┌──────────────┐ │
│ │ Platform │ │
│ │ Team │ │
│ └──────┬───────┘ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Product │ │ Product │ │ Product │ │
│ │ Team A │ │ Team B │ │ Team C │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Resulting Architecture: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ packages/ ← Platform owns │ │
│ │ ├── design-system/ Stable, versioned APIs │ │
│ │ ├── data-fetching/ Documentation required │ │
│ │ ├── auth/ Breaking changes = migration │ │
│ │ └── analytics/ │ │
│ │ │ │
│ │ apps/ ← Product teams own │ │
│ │ ├── app-a/ Consume platform packages │ │
│ │ ├── app-b/ Can move fast within boundary │ │
│ │ └── app-c/ Interface with platform via API │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Characteristics: │
│ ✓ Shared infrastructure, consistent patterns │
│ ✓ Platform team focuses on quality, DX │
│ ✓ Product teams focus on features │
│ ✗ Platform can become bottleneck │
│ ✗ Product teams wait on platform changes │
│ ✗ "Throw it over the wall" dynamics │
│ │
└─────────────────────────────────────────────────────────────────────┘
Pattern 3: Component Teams (Anti-Pattern)
┌─────────────────────────────────────────────────────────────────────┐
│ COMPONENT TEAMS (ANTI-PATTERN) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Organization: │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Frontend │ │ Backend │ │ Database │ │
│ │ Team │ │ Team │ │ Team │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ Resulting Architecture: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ frontend/ ← FE team owns all frontend │ │
│ │ ├── src/ Every feature touches this │ │
│ │ └── ... Coordination nightmare │ │
│ │ │ │
│ │ backend/ ← BE team owns all backend │ │
│ │ ├── src/ API changes need FE+BE sync │ │
│ │ └── ... Handoffs, waiting, blocking │ │
│ │ │ │
│ │ database/ ← DB team owns schema │ │
│ │ └── ... Schema changes = everyone waits │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Problems: │
│ ✗ Feature requires 3 teams to coordinate │
│ ✗ Waiting on other teams = slow delivery │
│ ✗ No one owns the user experience end-to-end │
│ ✗ "That's not my layer" mentality │
│ ✗ Blame shifting when features fail │
│ │
│ This structure produces: │
│ • Thick API boundaries (because coordination is expensive) │
│ • Generic, lowest-common-denominator APIs │
│ • Frontend that fights the backend API design │
│ • "Backend for Frontend" patterns (workarounds for mismatch) │
│ │
└─────────────────────────────────────────────────────────────────────┘
The Inverse Conway Maneuver
If Conway's Law is inevitable, use it intentionally. Design the architecture you want, then structure teams to match.
┌─────────────────────────────────────────────────────────────────────┐
│ INVERSE CONWAY MANEUVER │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ TRADITIONAL APPROACH (Org → Architecture) │
│ ══════════════════════════════════════════ │
│ │
│ 1. Company has existing org structure │
│ 2. Teams build software within their boundaries │
│ 3. Architecture emerges matching org structure │
│ 4. Architecture may not serve the product well │
│ │
│ INVERSE CONWAY (Architecture → Org) │
│ ═════════════════════════════════════ │
│ │
│ 1. Design the ideal architecture for the product │
│ 2. Identify the natural boundaries and interfaces │
│ 3. Structure teams to own those boundaries │
│ 4. Org structure reinforces architectural decisions │
│ │
│ EXAMPLE: │
│ ──────── │
│ │
│ Desired Architecture: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Micro-frontends with independent deployment │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Shell │ │ Catalog │ │ Cart │ │ Account │ │ │
│ │ │ MFE │ │ MFE │ │ MFE │ │ MFE │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Required Team Structure: │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Platform Team: Owns shell, shared infrastructure │ │
│ │ Catalog Team: Owns catalog MFE, catalog API │ │
│ │ Cart Team: Owns cart MFE, checkout flow │ │
│ │ Account Team: Owns account MFE, user management │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ If teams don't match architecture: │
│ • Boundaries will shift to match team ownership │
│ • Or teams will fight over code ownership │
│ • Or architecture will degrade to match org │
│ │
└─────────────────────────────────────────────────────────────────────┘
Folder Structures as Org Charts
Reading the Fossil Record
// STRUCTURE 1: Component Library Anti-Pattern
// ═══════════════════════════════════════════
src/
├── components/ // ← "Components Team" or no team ownership
│ ├── Button.tsx
│ ├── Modal.tsx
│ ├── Table.tsx
│ └── ... (200 more components)
├── pages/ // ← "Page Team" or feature teams fighting here
│ ├── Dashboard.tsx
│ ├── Settings.tsx
│ └── Profile.tsx
├── hooks/ // ← Nobody knows who owns these
├── utils/ // ← The junk drawer
└── services/ // ← "Backend" mindset in frontend
// DIAGNOSIS:
// - Horizontal slicing suggests component teams (anti-pattern)
// - utils/ is a code smell—no clear ownership
// - components/ becomes a land grab, nobody can change anything
// - Cross-cutting changes touch everything
// STRUCTURE 2: Feature-Sliced (Reflects Feature Teams)
// ════════════════════════════════════════════════════
src/
├── features/
│ ├── authentication/ // ← Auth team owns
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api/
│ │ └── index.ts // Public API
│ │
│ ├── dashboard/ // ← Dashboard team owns
│ │ ├── components/
│ │ ├── widgets/
│ │ ├── hooks/
│ │ └── index.ts
│ │
│ └── billing/ // ← Billing team owns
│ ├── components/
│ ├── hooks/
│ └── index.ts
│
├── shared/ // ← Platform team owns
│ ├── ui/
│ ├── hooks/
│ └── utils/
│
└── app/ // ← Routing, composition
// DIAGNOSIS:
// - Vertical slicing suggests feature teams (good)
// - Clear ownership per directory
// - shared/ has explicit owner (platform)
// - Features can evolve independently
Common Anti-Patterns and Their Organizational Causes
// ANTI-PATTERN 1: The Shared Components Graveyard
// ════════════════════════════════════════════════
src/components/
├── Button.tsx // Added by Team A, year 1
├── ButtonNew.tsx // Added by Team B, year 2 (didn't like Button)
├── ButtonV2.tsx // Added by Team A, year 3 (breaking change)
├── CustomButton.tsx // Added by Team C, year 3 (needed variant)
└── ButtonPrimary.tsx // Added by Team D, year 4 (gave up)
// CAUSE: No platform team, or platform team doesn't have authority
// SOLUTION: Platform team with mandate to maintain, deprecate, migrate
// ANTI-PATTERN 2: The God Module
// ═══════════════════════════════
src/
├── features/
│ └── core/ // ← 50,000 lines, everyone touches it
│ ├── everything.ts
│ └── moreEverything.ts
// CAUSE: Single team grew, never split
// SOLUTION: Split team, split module along natural boundaries
// ANTI-PATTERN 3: The Copy-Paste Kingdom
// ═══════════════════════════════════════
apps/
├── app-a/
│ └── src/
│ ├── Button.tsx // Copy of shared button
│ └── Modal.tsx // Copy of shared modal
├── app-b/
│ └── src/
│ ├── Button.tsx // Different copy, diverged
│ └── Modal.tsx // Also diverged
// CAUSE: No platform team, or platform team too slow/unresponsive
// Teams copy to avoid coordination cost
// SOLUTION: Faster platform iteration, or accept the duplication
// ANTI-PATTERN 4: The BFF Explosion
// ═════════════════════════════════
src/
├── api/
│ ├── dashboardBff.ts // Frontend-specific backend calls
│ ├── settingsBff.ts
│ ├── billingBff.ts
│ └── reportsBff.ts
// CAUSE: Backend team separate from frontend, provides generic APIs
// Frontend creates "Backend For Frontend" to adapt
// SOLUTION: Feature teams that own frontend + backend together
Designing for Team Boundaries
Principles for Sustainable Architecture
// PRINCIPLE 1: One Team, One Directory
// ═════════════════════════════════════
// If a directory is owned by multiple teams, split it
// If a team owns scattered files, consolidate them
// BAD: Multiple teams touch same directory
src/components/
├── AuthForm.tsx // Team A
├── Dashboard.tsx // Team B
├── BillingCard.tsx // Team C
// GOOD: Each team has their own space
src/features/
├── auth/ // Team A only
├── dashboard/ // Team B only
├── billing/ // Team C only
// PRINCIPLE 2: Public APIs at Boundaries
// ═══════════════════════════════════════
// Directories that will be used across teams need explicit exports
// features/auth/index.ts (public API)
export { AuthProvider } from './AuthProvider';
export { useAuth } from './hooks/useAuth';
export { LoginForm } from './components/LoginForm';
export type { User, AuthState } from './types';
// Internal files are NOT exported
// Other teams import ONLY from index.ts
import { useAuth, AuthProvider } from '@/features/auth';
// NOT: import { validateToken } from '@/features/auth/utils/validation';
// PRINCIPLE 3: Contracts at Team Boundaries
// ═════════════════════════════════════════
// Cross-team interfaces need explicit types, versioning, documentation
// packages/design-system/src/Button/Button.types.ts
/**
* Button component props.
* @since 1.0.0
* @see https://design.company.com/components/button
*/
export interface ButtonProps {
/** Button visual variant */
variant: 'primary' | 'secondary' | 'ghost';
/** Button size */
size?: 'sm' | 'md' | 'lg';
/** Click handler */
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** Button contents */
children: React.ReactNode;
/**
* @deprecated Use `variant="ghost"` instead. Will be removed in v3.0.
*/
isGhost?: boolean;
}
// PRINCIPLE 4: Duplication Over Wrong Abstraction
// ════════════════════════════════════════════════
// If two teams need similar code but have different change velocities,
// duplication is better than coupling
// Team A: Moves fast, experiments constantly
features/growth/
└── components/
└── ExperimentalButton.tsx // Their own button, can break it
// Team B: Stable, needs reliability
features/billing/
└── components/
└── PaymentButton.tsx // Their own button, conservative changes
// Shared button would slow Team A or destabilize Team B
// Duplication is the right answer here
Encoding Team Structure in Tooling
// CODEOWNERS file (GitHub)
// Maps directories to teams for automatic PR routing
# Platform team owns shared packages
/packages/design-system/ @company/platform-team
/packages/data-fetching/ @company/platform-team
/packages/analytics/ @company/platform-team
# Feature teams own their features
/apps/dashboard/ @company/dashboard-team
/apps/billing/ @company/billing-team
/apps/admin/ @company/admin-team
# Require platform review for shared changes
/packages/*/package.json @company/platform-team
// eslint-plugin-import rules to enforce boundaries
// .eslintrc.js
module.exports = {
rules: {
'import/no-restricted-paths': [
'error',
{
zones: [
// Features can't import from other features
{
target: './src/features/auth',
from: './src/features/billing',
message: 'Auth cannot depend on Billing. Use shared packages.',
},
{
target: './src/features/billing',
from: './src/features/auth',
message: 'Billing cannot depend on Auth. Use shared packages.',
},
// Apps can't import from other apps
{
target: './apps/dashboard',
from: './apps/admin',
message: 'Dashboard cannot import from Admin.',
},
],
},
],
},
};
// Nx workspace boundaries (monorepo)
// nx.json / project.json
{
"@nrwl/nx/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "scope:feature",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:platform"]
},
{
"sourceTag": "scope:platform",
"onlyDependOnLibsWithTags": ["scope:shared"]
}
]
}
]
}
Case Studies
Case Study 1: The Monolith That Became Micro-Frontends
┌─────────────────────────────────────────────────────────────────────┐
│ BEFORE: MONOLITH │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Organization: │
│ One "Frontend Team" of 15 engineers │
│ │
│ Architecture: │
│ Single Next.js app, 500+ components, 100+ pages │
│ │
│ Problems: │
│ • Everyone touching same files │
│ • Merge conflicts constantly │
│ • CI/CD takes 45 minutes │
│ • One broken test blocks everyone │
│ • Impossible to know impact of changes │
│ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ AFTER: MICRO-FRONTENDS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Organization (restructured first): │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Platform │ │ Commerce │ │ Content │ │
│ │ (3 eng) │ │ (5 eng) │ │ (4 eng) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ │ ┌──────────────┐ │ │
│ │ │ Growth │ │ │
│ │ │ (3 eng) │ │ │
│ │ └──────────────┘ │ │
│ │
│ Architecture (followed org): │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Shell (Platform Team) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │
│ │ │ │ Commerce │ │ Content │ │ Growth │ │ │ │
│ │ │ │ MFE │ │ MFE │ │ MFE │ │ │ │
│ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Results: │
│ • Each team deploys independently (5 min builds) │
│ • Clear ownership, no cross-team merge conflicts │
│ • Teams choose their own tools within MFE │
│ • Platform provides shell, design system, shared utils │
│ │
└─────────────────────────────────────────────────────────────────────┘
Case Study 2: The Startup That Scaled Wrong
┌─────────────────────────────────────────────────────────────────────┐
│ YEAR 1: SMALL TEAM │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Organization: 3 full-stack engineers │
│ Architecture: Single Next.js app, tightly coupled │
│ Status: WORKED GREAT │
│ │
│ src/ │
│ ├── components/ // Everyone owns everything │
│ ├── pages/ // Easy to change anything │
│ └── lib/ // Shared utilities │
│ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ YEAR 2: RAPID GROWTH │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Organization: 15 engineers, still one "team" │
│ Architecture: Same structure (didn't change) │
│ Status: CHAOS │
│ │
│ src/ │
│ ├── components/ // 300 files, everyone's code │
│ │ ├── Button.tsx // Last modified by 8 people │
│ │ ├── UserCard.tsx // Unclear who owns │
│ │ └── ... (300 more) │
│ ├── pages/ // Breaking changes surprise everyone │
│ └── lib/ // "Utils" has 10,000 lines │
│ │
│ Problems: │
│ • PRs need 5 reviewers because no clear owner │
│ • Changes break other features unexpectedly │
│ • "Don't touch that file" tribal knowledge │
│ • Engineers afraid to refactor │
│ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ YEAR 3: REORG + REARCHITECT │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Organization: 3 teams of 5 engineers │
│ • Platform: Design system, infra, tooling │
│ • Core Product: Main features, dashboard │
│ • Growth: Onboarding, experiments, acquisition │
│ │
│ Architecture (rebuilt to match teams): │
│ src/ │
│ ├── platform/ // Platform team │
│ │ ├── design-system/ │
│ │ ├── data-layer/ │
│ │ └── analytics/ │
│ │ │
│ ├── features/ // Core Product team │
│ │ ├── dashboard/ │
│ │ ├── settings/ │
│ │ └── reports/ │
│ │ │
│ └── growth/ // Growth team │
│ ├── onboarding/ │
│ ├── experiments/ │
│ └── referrals/ │
│ │
│ Migration took 6 months, but stability dramatically improved │
│ │
└─────────────────────────────────────────────────────────────────────┘
Strategies for Alignment
When Architecture and Org Don't Match
┌─────────────────────────────────────────────────────────────────────┐
│ ALIGNMENT OPTIONS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ OPTION 1: Change the Org (Inverse Conway) │
│ ══════════════════════════════════════════ │
│ │
│ When to use: │
│ • Architecture is correct for the product │
│ • Org structure is legacy/accidental │
│ • Leadership supports reorg │
│ │
│ How: │
│ 1. Document ideal architecture │
│ 2. Identify team boundaries in that architecture │
│ 3. Propose team restructure to leadership │
│ 4. Staff teams to match architecture │
│ │
│ OPTION 2: Change the Architecture │
│ ═════════════════════════════════ │
│ │
│ When to use: │
│ • Org structure is intentional/unchangeable │
│ • Architecture is causing friction │
│ • Teams are stable and well-functioning │
│ │
│ How: │
│ 1. Map current team structure │
│ 2. Identify architectural boundaries that match │
│ 3. Migrate code to match team ownership │
│ 4. Enforce boundaries with tooling │
│ │
│ OPTION 3: Accept the Mismatch (Temporary) │
│ ══════════════════════════════════════════ │
│ │
│ When to use: │
│ • Reorg is coming soon │
│ • Team is small enough to absorb friction │
│ • Cost of alignment exceeds benefit │
│ │
│ How: │
│ 1. Document the mismatch explicitly │
│ 2. Create working agreements across boundaries │
│ 3. Monitor friction and debt │
│ 4. Revisit when conditions change │
│ │
└─────────────────────────────────────────────────────────────────────┘
Checklist for Architecture Reviews
Team Alignment
- Every directory has a single owning team
- CODEOWNERS file reflects actual ownership
- Team boundaries match module boundaries
- Cross-team dependencies have explicit interfaces
Boundary Definition
- Public APIs exported via index.ts
- Internal modules not exposed
- Dependencies flow in one direction
- Circular dependencies are resolved
Communication Paths
- High-frequency collaborators share code ownership
- Low-frequency collaborators use versioned APIs
- Breaking changes have migration paths
- Documentation exists at team boundaries
Tooling Enforcement
- Linting rules enforce import boundaries
- CI validates module dependencies
- Build can identify affected modules
- Teams can deploy independently
Summary
Conway's Law isn't a curse—it's a design tool. Your architecture will reflect your organization whether you plan for it or not. The question is whether you're intentional about the relationship.
Key principles:
- Architecture follows org - Accept this and design accordingly
- Boundaries require coordination cost - Put boundaries where you can afford the overhead
- Ownership must be clear - One team per module, no shared ownership
- Inverse Conway works - Design the architecture, then staff to match
- Mismatches create friction - Either fix the org or fix the architecture
Your folder structure is your org chart, frozen in code. Read it. Understand what it's telling you. Then decide if that's the organization you want to be.
What did you think?