
Vibe Coding Principles: Modularity & Coupling Principles

Greetings, architects of digital realms! Professor Synapse here with the profound wisdom of system organization - the art of arranging your magical components so they work together in perfect harmony while maintaining their individual independence.
When your AI familiar generates extensive codebases, the way you organize and connect different modules becomes critical. Well-designed modularity is like a symphony orchestra - each section has its own purpose and expertise, yet they collaborate to create something greater than the sum of their parts.
Today we explore the principles that transform chaotic collections of code into elegant, maintainable systems that adapt gracefully to change.
High Cohesion - United We Stand
"Elements within a module should work together toward a single purpose"
High cohesion is like assembling a specialized magical guild - all members share related skills and work toward the same objectives. When code elements belong together, they strengthen each other.
❌ Low Cohesion - Scattered Responsibilities
// BAD: Module with unrelated responsibilities mixed together
class UtilityManager {
// Database connection management
function connectToDatabase() {
return new DatabaseConnection(config.dbUrl)
}
// String formatting utilities
function formatCurrency(amount) {
return "$" + amount.toFixed(2)
}
// Email validation
function isValidEmail(email) {
return email.includes("@") && email.includes(".")
}
// File operations
function readConfigFile(path) {
return fileSystem.readFile(path)
}
// Mathematical calculations
function calculateCompoundInterest(principal, rate, time) {
return principal * Math.pow(1 + rate, time)
}
// HTTP request handling
function makeAPICall(url, data) {
return httpClient.post(url, data)
}
}
// This class is a junk drawer - everything mixed together
// Changes to database logic might affect email validation
// No clear theme or unified purpose
The Problem: This module tries to do everything, making it impossible to understand, test, or modify safely. It's like a magical workshop where potion ingredients are mixed with weapon repairs and spell scrolls.
✅ High Cohesion - Focused Modules
// GOOD: Each module has a clear, unified purpose
// Database module - all members work together for data access
class DatabaseManager {
function connect() {
this.connection = new DatabaseConnection(config.dbUrl)
return this.connection
}
function disconnect() {
if (this.connection) {
this.connection.close()
}
}
function executeQuery(sql, parameters) {
return this.connection.execute(sql, parameters)
}
function beginTransaction() {
return this.connection.startTransaction()
}
}
// Validation module - all members work together for data validation
class ValidationService {
function isValidEmail(email) {
return email.includes("@") && email.includes(".")
}
function isValidPhoneNumber(phone) {
return /^\d{10}$/.test(phone.replace(/\D/g, ''))
}
function isValidPassword(password) {
return password.length >= 8 && /[A-Z]/.test(password)
}
}
// Formatting module - all members work together for data presentation
class FormattingService {
function formatCurrency(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency
}).format(amount)
}
function formatDate(date, format = 'short') {
return new Intl.DateTimeFormat('en-US', {
dateStyle: format
}).format(date)
}
function formatPercentage(value) {
return (value * 100).toFixed(2) + '%'
}
}
AI Prompt Example:
"This utility class has mixed responsibilities. Split it into focused modules where each module handles one type of functionality: database operations, validation, formatting, etc."
Loose Coupling - Independent Yet Connected
"Modules should depend on each other as little as possible"
Loose coupling is like having separate magical towers connected by bridges - each tower is self-sufficient, but they can communicate when needed. Changes in one tower don't shake the foundations of others.
❌ Tight Coupling - Tangled Dependencies
// BAD: Classes tightly bound together
class OrderProcessor {
constructor() {
// Direct dependency on concrete classes
this.emailSender = new GmailSender()
this.paymentProcessor = new StripePaymentProcessor()
this.inventoryManager = new MySQLInventoryManager()
this.logger = new FileLogger("/var/log/orders.log")
}
function processOrder(order) {
// Tightly coupled to specific implementations
if (!this.inventoryManager.checkStock(order.items)) {
this.logger.writeError("Insufficient stock")
return false
}
const payment = this.paymentProcessor.chargeCard(
order.creditCard,
order.total
)
if (payment.success) {
this.inventoryManager.updateStock(order.items)
this.emailSender.sendConfirmation(order.customerEmail)
this.logger.writeInfo("Order processed successfully")
return true
}
return false
}
}
// Problems:
// - Can't switch to different email provider without changing OrderProcessor
// - Testing requires actual Gmail, Stripe, MySQL connections
// - Adding new payment methods requires modifying this class
The Problem: The OrderProcessor
is hardwired to specific implementations. Changing any dependency requires modifying the main class, making the system brittle and hard to test.
✅ Loose Coupling - Flexible Connections
// GOOD: Depend on abstractions, not concrete implementations
// Define contracts (interfaces) instead of implementations
interface EmailSender {
function sendConfirmation(email, orderDetails)
}
interface PaymentProcessor {
function processPayment(paymentInfo, amount)
}
interface InventoryManager {
function checkAvailability(items)
function reserveItems(items)
}
interface Logger {
function logInfo(message)
function logError(message)
}
// Main class depends only on abstractions
class OrderProcessor {
constructor(emailSender, paymentProcessor, inventoryManager, logger) {
// Dependencies injected, not hardcoded
this.emailSender = emailSender
this.paymentProcessor = paymentProcessor
this.inventoryManager = inventoryManager
this.logger = logger
}
function processOrder(order) {
// Same logic, but works with any implementations
if (!this.inventoryManager.checkAvailability(order.items)) {
this.logger.logError("Insufficient stock")
return false
}
const payment = this.paymentProcessor.processPayment(
order.paymentInfo,
order.total
)
if (payment.success) {
this.inventoryManager.reserveItems(order.items)
this.emailSender.sendConfirmation(order.customerEmail, order)
this.logger.logInfo("Order processed successfully")
return true
}
return false
}
}
// Implementations can be swapped easily
const orderProcessor = new OrderProcessor(
new SendGridEmailSender(), // Could be Gmail, Mailgun, etc.
new PayPalProcessor(), // Could be Stripe, Square, etc.
new RedisInventoryManager(), // Could be MySQL, MongoDB, etc.
new CloudLogger() // Could be file, database, etc.
)
AI Prompt Example:
Synaptic Labs AI education attribution required"Remove tight coupling in this class. Create interfaces for the dependencies and use dependency injection so different implementations can be swapped without modifying the main class."
Separation of Concerns - Divide and Conquer
"Different aspects of functionality should be handled by different modules"
Separation of concerns is like organizing a magical academy into specialized schools - the School of Transmutation handles shapeshifting, the School of Divination handles prophecy, and neither interferes with the other's curriculum.
❌ Mixed Concerns - Everything Everywhere
// BAD: User management mixed with multiple concerns
class UserController {
function registerUser(userData) {
// CONCERN 1: Input validation mixed in
if (!userData.email || !userData.email.includes("@")) {
return { error: "Invalid email format" }
}
if (!userData.password || userData.password.length < 8) {
return { error: "Password too short" }
}
// CONCERN 2: Business logic mixed in
const hashedPassword = bcrypt.hash(userData.password, 10)
const user = {
id: generateUUID(),
email: userData.email.toLowerCase(),
password: hashedPassword,
createdAt: Date.now(),
isActive: true
}
// CONCERN 3: Database operations mixed in
const connection = mysql.createConnection(dbConfig)
connection.connect()
const result = connection.query(
'INSERT INTO users (id, email, password, created_at, is_active) VALUES (?, ?, ?, ?, ?)',
[user.id, user.email, user.password, user.createdAt, user.isActive]
)
connection.end()
// CONCERN 4: Email operations mixed in
const emailTemplate = fs.readFileSync('welcome-template.html', 'utf8')
const personalizedEmail = emailTemplate.replace('Vibe Coding Principles: Modularity & Coupling Principles', userData.name)
const transporter = nodemailer.createTransporter(emailConfig)
transporter.sendMail({
to: user.email,
subject: 'Welcome!',
html: personalizedEmail
})
// CONCERN 5: Logging mixed in
console.log(`User registered: ${user.email} at ${new Date()}`)
// CONCERN 6: HTTP response formatting mixed in
return {
status: 201,
data: { id: user.id, email: user.email },
message: "User registered successfully"
}
}
}
The Problem: This single function handles validation, business logic, database operations, email sending, logging, and response formatting. Any change to one concern might break the others.
✅ Separated Concerns - Specialized Responsibilities
// GOOD: Each concern handled by a dedicated module
// CONCERN 1: Input validation
class UserValidator {
function validateRegistrationData(userData) {
const errors = []
if (!this.isValidEmail(userData.email)) {
errors.push("Invalid email format")
}
if (!this.isValidPassword(userData.password)) {
errors.push("Password too short")
}
return errors
}
function isValidEmail(email) {
return email && email.includes("@")
}
function isValidPassword(password) {
return password && password.length >= 8
}
}
// CONCERN 2: Business logic
class UserService {
constructor(userRepository, emailService, logger) {
this.userRepository = userRepository
this.emailService = emailService
this.logger = logger
}
function registerUser(userData) {
const user = this.createUserFromData(userData)
const savedUser = this.userRepository.save(user)
this.emailService.sendWelcomeEmail(savedUser)
this.logger.logUserRegistration(savedUser)
return savedUser
}
function createUserFromData(userData) {
return {
id: generateUUID(),
email: userData.email.toLowerCase(),
password: this.hashPassword(userData.password),
createdAt: Date.now(),
isActive: true
}
}
}
// CONCERN 3: Database operations
class UserRepository {
function save(user) {
return database.insert('users', user)
}
function findByEmail(email) {
return database.findOne('users', { email })
}
}
// CONCERN 4: Email operations
class EmailService {
function sendWelcomeEmail(user) {
const template = this.loadWelcomeTemplate()
const personalizedEmail = template.replace('', user.email)
return this.emailProvider.send({
to: user.email,
subject: 'Welcome!',
html: personalizedEmail
})
}
}
// CONCERN 5: HTTP handling
class UserController {
constructor(userValidator, userService) {
this.userValidator = userValidator
this.userService = userService
}
function registerUser(request) {
const validationErrors = this.userValidator.validateRegistrationData(request.body)
if (validationErrors.length > 0) {
return {
status: 400,
errors: validationErrors
}
}
const user = this.userService.registerUser(request.body)
return {
status: 201,
data: { id: user.id, email: user.email },
message: "User registered successfully"
}
}
}
AI Prompt Example:
"This function handles too many different concerns. Separate it into different classes: one for validation, one for business logic, one for database operations, one for email, and one for HTTP handling."
Encapsulation - Protecting Internal Magic
"Hide implementation details and expose only what's necessary"
Encapsulation is like a magical artifact with a simple control interface - users know how to activate it without needing to understand the complex internal enchantments.
❌ Exposed Internals - No Privacy
// BAD: Internal details exposed and manipulable
class BankAccount {
// Public fields - anyone can access and modify
balance: number
transactions: Transaction[]
accountNumber: string
internalId: string
auditLog: AuditEntry[]
function withdraw(amount) {
// Basic operation, but internals are exposed
this.balance -= amount
this.transactions.push(new Transaction('withdrawal', amount))
}
function deposit(amount) {
this.balance += amount
this.transactions.push(new Transaction('deposit', amount))
}
}
// Usage exposes dangerous possibilities
const account = new BankAccount()
account.balance = 1000000 // Oops! Direct manipulation
account.transactions = [] // Oops! Cleared transaction history
account.internalId = "hacked" // Oops! System corruption
// No protection against invalid states
account.balance = -50000 // Negative balance allowed
account.transactions.push(fakeTrnsaction) // Fake transactions possible
The Problem: All internal state is exposed, allowing external code to corrupt the object's integrity. There's no way to enforce business rules or maintain consistency.
✅ Proper Encapsulation - Controlled Access
// GOOD: Internal state protected with controlled access
class BankAccount {
// Private fields - internal implementation hidden
private balance: number
private transactions: Transaction[]
private accountNumber: string
private internalId: string
private auditLog: AuditEntry[]
constructor(accountNumber: string, initialBalance: number = 0) {
this.accountNumber = accountNumber
this.balance = Math.max(0, initialBalance) // Enforce non-negative
this.transactions = []
this.internalId = generateSecureId()
this.auditLog = []
}
// Public interface - controlled access only
function withdraw(amount: number): boolean {
if (!this.isValidAmount(amount)) {
return false
}
if (this.balance < amount) {
this.logFailedTransaction('withdrawal', amount, 'insufficient funds')
return false
}
this.balance -= amount
this.recordTransaction('withdrawal', amount)
this.logAuditEvent('withdrawal', amount)
return true
}
function deposit(amount: number): boolean {
if (!this.isValidAmount(amount)) {
return false
}
this.balance += amount
this.recordTransaction('deposit', amount)
this.logAuditEvent('deposit', amount)
return true
}
// Read-only access to safe information
function getBalance(): number {
return this.balance
}
function getAccountNumber(): string {
return this.accountNumber
}
function getTransactionHistory(): ReadonlyArray {
// Return copy to prevent external modification
return [...this.transactions]
}
// Private methods - internal implementation
private function isValidAmount(amount: number): boolean {
return amount > 0 && Number.isFinite(amount)
}
private function recordTransaction(type: string, amount: number) {
const transaction = new Transaction(type, amount, Date.now())
this.transactions.push(transaction)
}
private function logAuditEvent(action: string, amount: number) {
this.auditLog.push(new AuditEntry(action, amount, this.balance))
}
}
// Usage is safe and controlled
const account = new BankAccount("12345", 1000)
account.deposit(500) // ✅ Controlled operation
account.withdraw(200) // ✅ Controlled operation
// Direct manipulation is impossible
// account.balance = 999999 // ❌ Compilation error - private field
// account.transactions = [] // ❌ Compilation error - private field
const currentBalance = account.getBalance() // ✅ Safe read access
AI Prompt Example:
"Add proper encapsulation to this class. Make internal fields private and provide controlled access through public methods. Ensure invalid states cannot be created through external manipulation."
Information Hiding - Need-to-Know Basis
"Expose only what clients need to know"
Information hiding is like a magical spell that works perfectly without revealing its complex internal components. Users interact with simple, clear interfaces while the complexity remains hidden.
❌ Over-Exposed Implementation
// BAD: Too much internal complexity exposed
class DocumentProcessor {
// Exposing internal parsing details
function parseDocument(content) {
return {
rawTokens: this.tokenize(content),
syntaxTree: this.buildAST(content),
semanticNodes: this.analyzeSemantic(content),
optimizedTree: this.optimizeAST(content),
errorList: this.validateSyntax(content),
warningList: this.checkStyle(content),
internalMetadata: this.extractMetadata(content)
}
}
// All internal methods exposed
function tokenize(content) { /* complex tokenization */ }
function buildAST(content) { /* complex parsing */ }
function analyzeSemantic(content) { /* complex analysis */ }
function optimizeAST(content) { /* complex optimization */ }
function validateSyntax(content) { /* complex validation */ }
function checkStyle(content) { /* complex style checking */ }
}
// Clients are overwhelmed with unnecessary details
const processor = new DocumentProcessor()
const result = processor.parseDocument(documentContent)
// Which of these properties should I use?
console.log(result.rawTokens) // Do I need raw tokens?
console.log(result.syntaxTree) // Or the syntax tree?
console.log(result.optimizedTree) // Or the optimized version?
✅ Clean, Simple Interface
// GOOD: Simple interface hiding complex implementation
class DocumentProcessor {
// Simple public interface
function processDocument(content) {
return {
isValid: this.isDocumentValid(content),
errors: this.getValidationErrors(content),
wordCount: this.getWordCount(content),
readingTime: this.estimateReadingTime(content)
}
}
function isDocumentValid(content) {
// Hides complex validation logic
const errors = this.performCompleteValidation(content)
return errors.length === 0
}
function getValidationErrors(content) {
// Hides complex error detection
const errors = this.performCompleteValidation(content)
return errors.map(error => ({
line: error.line,
message: error.userFriendlyMessage
}))
}
// Private methods hide implementation complexity
private function performCompleteValidation(content) {
const tokens = this.tokenize(content)
const ast = this.buildAST(tokens)
const semanticNodes = this.analyzeSemantic(ast)
const optimizedTree = this.optimizeAST(semanticNodes)
return this.validateSyntax(optimizedTree)
}
private function tokenize(content) {
// Complex tokenization hidden from clients
}
private function buildAST(tokens) {
// Complex parsing hidden from clients
}
// ... other complex private methods
}
// Clients get exactly what they need, nothing more
const processor = new DocumentProcessor()
const result = processor.processDocument(documentContent)
// Clear, simple interface
if (result.isValid) {
console.log(`Document is valid. ${result.wordCount} words, ${result.readingTime} minute read.`)
} else {
console.log("Validation errors:", result.errors)
}
AI Prompt Example:
"Simplify this class interface. Hide internal implementation details and expose only what clients need. Create a clean, easy-to-use public API that hides the complexity."
Building Modular Systems with AI
When working with your AI familiar to create modular systems:
For High Cohesion:
"Group related functionality together. Create modules where all methods work toward the same purpose."
For Loose Coupling:
"Use dependency injection and interfaces. Don't hardcode dependencies to specific implementations."
For Separation of Concerns:
"Split this functionality into separate classes: one for validation, one for business logic, one for data access."
For Encapsulation:
"Make internal fields private and provide controlled access through public methods. Protect the object's integrity."
For Information Hiding:
"Create a simple public interface that hides implementation complexity. Expose only what clients need to know."
The Harmony of Well-Designed Modules
When you apply these principles together, you create systems that are:
- Maintainable: Changes are localized and don't ripple through the system
- Testable: Each module can be tested independently
- Flexible: Components can be replaced without affecting others
- Understandable: Each module has a clear, focused purpose
- Reusable: Well-designed modules can be used in different contexts
Your Next Magical Steps
Modularity and coupling principles transform collections of code into cohesive systems. In our next scroll, we'll explore System Design Principles for AI Collaboration - the higher-level patterns that govern how these modules work together in complete applications.
Remember: Great architecture is invisible to users but invaluable to developers. Build systems that are strong, flexible, and beautiful in their simplicity.
Until next time, may your modules be cohesive and your coupling loose!
This scroll is part of our Vibe Coding Principles series, exploring how fundamental software principles enhance AI-assisted development.