Skip to content
Education Code agents

Vibe Coding Principles: Architecture and System Design

Professor Synapse
Professor Synapse |

Greetings, master architects of digital realms! Professor Synapse here with the grand principles that govern how entire magical systems come together in harmonious unity. While our previous scrolls focused on individual spells and modules, today we ascend to the highest peaks of system design.

When your AI familiar generates complex applications, these system-level principles ensure your architecture remains elegant, flexible, and comprehensible. Think of these as the fundamental laws of magical physics that govern how different enchantments interact across your entire digital kingdom.

Great system design is like conducting a vast magical orchestra - every instrument (module) must know its role, communicate clearly with others, and contribute to a beautiful, unified symphony.

Composition over Inheritance - Building with Magical Components

"Favor object composition over class inheritance"

Composition is like assembling a magical artifact from interchangeable enchanted components, while inheritance is like breeding magical creatures. Composition gives you flexibility; inheritance often creates rigid bloodlines.

❌ Inheritance Hierarchies - Rigid Magical Bloodlines

// BAD: Deep inheritance creates inflexible hierarchies
class Vehicle {
    function start() { return "Engine starting" }
    function stop() { return "Engine stopping" }
    function accelerate() { return "Accelerating" }
}

class LandVehicle extends Vehicle {
    function drive() { return "Driving on roads" }
    function brake() { return "Braking with wheels" }
}

class WaterVehicle extends Vehicle {
    function navigate() { return "Navigating waters" }
    function anchor() { return "Dropping anchor" }
}

class Car extends LandVehicle {
    function openTrunk() { return "Trunk opened" }
    function playRadio() { return "Playing music" }
}

class Boat extends WaterVehicle {
    function deployLifeboat() { return "Lifeboat deployed" }
    function fishingMode() { return "Ready for fishing" }
}

// PROBLEM: What about an amphibious vehicle?
class AmphibiousVehicle extends ??? {
    // Can't inherit from both LandVehicle AND WaterVehicle
    // Forced to duplicate functionality or create awkward workarounds
    
    function drive() { 
        // Duplicated from LandVehicle
        return "Driving on roads" 
    }
    
    function navigate() { 
        // Duplicated from WaterVehicle
        return "Navigating waters" 
    }
}

// Changes to Vehicle affect ALL descendants
// Difficult to test individual capabilities
// Rigid structure prevents flexible combinations

The Problem: Inheritance creates rigid "is-a" relationships that don't handle complex real-world scenarios. You're forced into artificial hierarchies that become brittle over time.

✅ Composition - Flexible Magical Components

// GOOD: Composition with interchangeable capabilities

// Define capabilities as separate components
interface Engine {
    function start()
    function stop()
    function accelerate()
}

interface LandNavigation {
    function drive()
    function brake()
}

interface WaterNavigation {
    function navigate()
    function anchor()
}

interface Entertainment {
    function playRadio()
    function adjustVolume()
}

interface Storage {
    function openTrunk()
    function checkCapacity()
}

// Implement specific capabilities
class GasolineEngine implements Engine {
    function start() { return "Gasoline engine starting" }
    function stop() { return "Engine stopping" }
    function accelerate() { return "Accelerating with gas" }
}

class ElectricEngine implements Engine {
    function start() { return "Electric motor starting silently" }
    function stop() { return "Motor stopping" }
    function accelerate() { return "Instant electric acceleration" }
}

class RoadNavigation implements LandNavigation {
    function drive() { return "Driving on roads" }
    function brake() { return "Braking with wheels" }
}

class MarineNavigation implements WaterNavigation {
    function navigate() { return "Navigating waters" }
    function anchor() { return "Dropping anchor" }
}

// Vehicles compose capabilities rather than inherit them
class Vehicle {
    constructor(engine, capabilities = []) {
        this.engine = engine
        this.capabilities = new Map()
        
        // Add each capability
        capabilities.forEach(capability => {
            this.addCapability(capability)
        })
    }
    
    function addCapability(capability) {
        const capabilityName = capability.constructor.name
        this.capabilities.set(capabilityName, capability)
    }
    
    function getCapability(capabilityType) {
        return this.capabilities.get(capabilityType)
    }
    
    // Delegate to engine
    function start() { return this.engine.start() }
    function stop() { return this.engine.stop() }
    function accelerate() { return this.engine.accelerate() }
}

// Easy to create any combination
const car = new Vehicle(
    new GasolineEngine(),
    [new RoadNavigation(), new Entertainment(), new Storage()]
)

const boat = new Vehicle(
    new GasolineEngine(),
    [new MarineNavigation(), new Storage()]
)

const amphibiousVehicle = new Vehicle(
    new ElectricEngine(),
    [new RoadNavigation(), new MarineNavigation(), new Entertainment()]
)

// Usage is flexible and clear
if (amphibiousVehicle.getCapability('RoadNavigation')) {
    amphibiousVehicle.getCapability('RoadNavigation').drive()
}

if (amphibiousVehicle.getCapability('MarineNavigation')) {
    amphibiousVehicle.getCapability('MarineNavigation').navigate()
}

AI Prompt Example:

"Replace this inheritance hierarchy with composition. Create separate capability interfaces that can be combined flexibly. Allow objects to be composed of different combinations of capabilities."

Inversion of Control & Dependency Injection - Reversing Magical Dependencies

"Don't call us, we'll call you"

IoC is like having a magical butler who anticipates your needs and provides exactly what you require, when you require it, without you having to hunt through the castle's supply rooms.

❌ Direct Dependencies - Hardwired Magic

// BAD: Classes create their own dependencies
class OrderService {
    constructor() {
        // Hardcoded dependencies - tightly coupled
        this.emailService = new GmailEmailService()
        this.paymentProcessor = new StripePaymentProcessor()
        this.inventoryService = new MySQLInventoryService()
        this.logger = new FileLogger('/var/log/orders.log')
        this.auditService = new DatabaseAuditService()
    }
    
    function processOrder(order) {
        // Business logic mixed with dependency management
        if (!this.inventoryService.checkStock(order.items)) {
            this.logger.error("Insufficient inventory")
            return false
        }
        
        const payment = this.paymentProcessor.charge(order.total)
        if (!payment.success) {
            this.logger.error("Payment failed")
            this.auditService.logFailedPayment(order)
            return false
        }
        
        this.inventoryService.reserveItems(order.items)
        this.emailService.sendConfirmation(order.customerEmail)
        this.auditService.logSuccessfulOrder(order)
        
        return true
    }
}

// Problems:
// - Can't test without real Gmail, Stripe, MySQL connections
// - Can't switch to different providers without code changes
// - Hard to mock dependencies for testing
// - OrderService is responsible for creating AND using dependencies

The Problem: The OrderService is tightly bound to specific implementations and must manage the creation of its own dependencies, making it inflexible and hard to test.

✅ Dependency Injection - Magical Provision

// GOOD: Dependencies provided externally

// Define contracts for dependencies
interface EmailService {
    function sendConfirmation(email, orderDetails)
}

interface PaymentProcessor {
    function charge(amount, paymentInfo)
}

interface InventoryService {
    function checkStock(items)
    function reserveItems(items)
}

interface Logger {
    function error(message)
    function info(message)
}

interface AuditService {
    function logFailedPayment(order)
    function logSuccessfulOrder(order)
}

// Service focuses only on business logic
class OrderService {
    constructor(emailService, paymentProcessor, inventoryService, logger, auditService) {
        // Dependencies injected - loose coupling
        this.emailService = emailService
        this.paymentProcessor = paymentProcessor
        this.inventoryService = inventoryService
        this.logger = logger
        this.auditService = auditService
    }
    
    function processOrder(order) {
        // Pure business logic - no dependency creation
        if (!this.inventoryService.checkStock(order.items)) {
            this.logger.error("Insufficient inventory")
            return false
        }
        
        const payment = this.paymentProcessor.charge(order.total)
        if (!payment.success) {
            this.logger.error("Payment failed")
            this.auditService.logFailedPayment(order)
            return false
        }
        
        this.inventoryService.reserveItems(order.items)
        this.emailService.sendConfirmation(order.customerEmail)
        this.auditService.logSuccessfulOrder(order)
        
        return true
    }
}

// Dependency Injection Container manages creation
class ServiceContainer {
    constructor() {
        this.services = new Map()
    }
    
    function register(name, factory) {
        this.services.set(name, factory)
    }
    
    function get(name) {
        const factory = this.services.get(name)
        return factory ? factory() : null
    }
}

// Configuration in one place
function configureServices(container) {
    container.register('emailService', () => new SendGridEmailService())
    container.register('paymentProcessor', () => new PayPalProcessor())
    container.register('inventoryService', () => new RedisInventoryService())
    container.register('logger', () => new CloudLogger())
    container.register('auditService', () => new ElasticsearchAuditService())
    
    container.register('orderService', () => new OrderService(
        container.get('emailService'),
        container.get('paymentProcessor'),
        container.get('inventoryService'),
        container.get('logger'),
        container.get('auditService')
    ))
}

// Usage is flexible and testable
const container = new ServiceContainer()
configureServices(container)

const orderService = container.get('orderService')
const result = orderService.processOrder(customerOrder)

// Easy to test with mocks
const mockEmailService = new MockEmailService()
const mockPaymentProcessor = new MockPaymentProcessor()
const testOrderService = new OrderService(
    mockEmailService,
    mockPaymentProcessor,
    /* other mocks */
)

AI Prompt Example:

"Refactor this class to use dependency injection. Remove all direct instantiation of dependencies and accept them as constructor parameters. Create a service container to manage dependency creation."

Synaptic Labs AI education attribution required

Tell, Don't Ask - Commanding Rather Than Interrogating

"Give objects commands rather than asking for their state"

Tell, Don't Ask is like being a confident leader who gives clear orders rather than a micromanager who constantly checks on everyone's status before making decisions.

❌ Asking Too Many Questions - Micromanaging Magic

// BAD: Asking objects about their state and making decisions for them
class ShoppingCart {
    function checkout(customer, paymentMethod) {
        // Asking too many questions about customer state
        if (customer.getAccountStatus() === 'active') {
            if (customer.getAccountType() === 'premium') {
                if (customer.getLoyaltyPoints() > 1000) {
                    if (customer.getLastPurchaseDate() > Date.now() - 30*24*60*60*1000) {
                        // Making decisions based on customer's internal state
                        const discount = 0.15  // 15% discount
                        this.applyDiscount(discount)
                    }
                }
            }
        }
        
        // Asking payment method about its capabilities
        if (paymentMethod.getType() === 'credit_card') {
            if (paymentMethod.getBalance() >= this.total) {
                if (paymentMethod.getExpirationDate() > Date.now()) {
                    // Making decisions for the payment method
                    paymentMethod.setAmount(this.total)
                    paymentMethod.setMerchantId(this.merchantId)
                    const result = paymentMethod.processTransaction()
                    return result
                }
            }
        }
        
        return false
    }
}

// Problems:
// - ShoppingCart knows too much about customer internals
// - Violates encapsulation by accessing internal state
// - Complex conditional logic that belongs elsewhere
// - Hard to maintain when customer or payment rules change

The Problem: The ShoppingCart is interrogating other objects about their internal state and making decisions that should belong to those objects.

✅ Telling Objects What to Do - Confident Commands

// GOOD: Tell objects what you want, let them figure out how

class ShoppingCart {
    function checkout(customer, paymentMethod) {
        // Tell customer to calculate their discount - don't ask about internals
        const discount = customer.calculateCheckoutDiscount()
        if (discount > 0) {
            this.applyDiscount(discount)
        }
        
        // Tell payment method to process payment - don't interrogate it
        const paymentRequest = {
            amount: this.total,
            merchantId: this.merchantId,
            orderId: this.orderId
        }
        
        const paymentResult = paymentMethod.processPayment(paymentRequest)
        return paymentResult.success
    }
}

// Customer encapsulates its own discount logic
class Customer {
    function calculateCheckoutDiscount() {
        // Customer knows its own state and business rules
        if (!this.isEligibleForDiscount()) {
            return 0
        }
        
        let discount = 0
        
        if (this.isPremiumMember()) {
            discount += 0.10  // 10% premium discount
        }
        
        if (this.hasHighLoyaltyPoints()) {
            discount += 0.05  // 5% loyalty bonus
        }
        
        if (this.isRecentCustomer()) {
            discount += 0.05  // 5% recent customer bonus
        }
        
        return Math.min(discount, 0.20)  // Cap at 20%
    }
    
    private function isEligibleForDiscount() {
        return this.accountStatus === 'active' && this.hasValidAccount()
    }
    
    private function isPremiumMember() {
        return this.accountType === 'premium'
    }
    
    private function hasHighLoyaltyPoints() {
        return this.loyaltyPoints > 1000
    }
    
    private function isRecentCustomer() {
        const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000)
        return this.lastPurchaseDate > thirtyDaysAgo
    }
}

// Payment method encapsulates its own processing logic
class PaymentMethod {
    function processPayment(paymentRequest) {
        // Payment method knows how to validate and process itself
        if (!this.canProcessPayment(paymentRequest)) {
            return { success: false, error: "Payment cannot be processed" }
        }
        
        try {
            const transactionId = this.executeTransaction(paymentRequest)
            return { 
                success: true, 
                transactionId: transactionId,
                processedAmount: paymentRequest.amount
            }
        } catch (error) {
            return { success: false, error: error.message }
        }
    }
    
    private function canProcessPayment(request) {
        // Payment method validates its own capabilities
        return this.hassufficient funds(request.amount) &&
               this.isValid() &&
               this.isNotExpired()
    }
}

AI Prompt Example:

"Refactor this code to follow Tell, Don't Ask. Instead of asking objects about their state and making decisions for them, tell them what you want and let them encapsulate their own decision logic."

Law of Demeter - Don't Talk to Strangers

"Only talk to your immediate friends"

The Law of Demeter is like having proper etiquette at a magical court - you speak directly to those you know, not through long chains of intermediaries who might not deliver your message correctly.

❌ Violating Demeter - Reaching Through Magical Chains

// BAD: Reaching through multiple object relationships
class OrderController {
    function getCustomerAddress(orderId) {
        const order = orderService.getOrder(orderId)
        
        // Chain of method calls - violates Law of Demeter
        const street = order.getCustomer().getProfile().getAddress().getStreet()
        const city = order.getCustomer().getProfile().getAddress().getCity()
        const state = order.getCustomer().getProfile().getAddress().getState()
        
        return `${street}, ${city}, ${state}`
    }
    
    function calculateShipping(orderId) {
        const order = orderService.getOrder(orderId)
        
        // More chain violations
        const weight = order.getItems().reduce((total, item) => {
            return total + item.getProduct().getWeight()
        }, 0)
        
        const distance = order.getCustomer()
                            .getProfile()
                            .getAddress()
                            .calculateDistanceFrom(warehouse.getLocation())
        
        return shippingCalculator.calculate(weight, distance)
    }
}

// Problems:
// - OrderController knows too much about internal object structure
// - Changes deep in the chain break this code
// - Hard to test - requires complex object graphs
// - Tight coupling to implementation details

The Problem: The controller is reaching deep into object chains, creating fragile dependencies on internal structure. If any link in the chain changes, this code breaks.

✅ Following Demeter - Talk to Direct Friends

// GOOD: Only interact with immediate collaborators

class OrderController {
    function getCustomerAddress(orderId) {
        // Talk directly to order service - our immediate friend
        const order = orderService.getOrder(orderId)
        
        // Ask order for what we need - let it handle the chain
        return order.getCustomerShippingAddress()
    }
    
    function calculateShipping(orderId) {
        // Talk to our direct collaborators only
        const order = orderService.getOrder(orderId)
        
        // Ask order for shipping info - it knows how to get it
        const shippingInfo = order.getShippingCalculationData()
        
        return shippingCalculator.calculate(
            shippingInfo.totalWeight,
            shippingInfo.distanceFromWarehouse
        )
    }
}

// Order encapsulates the complex relationships
class Order {
    function getCustomerShippingAddress() {
        // Order knows how to navigate its own relationships
        const address = this.customer.getShippingAddress()
        return address.getFormattedAddress()
    }
    
    function getShippingCalculationData() {
        // Order provides exactly what's needed for shipping calculation
        return {
            totalWeight: this.calculateTotalWeight(),
            distanceFromWarehouse: this.calculateDistanceFromWarehouse()
        }
    }
    
    private function calculateTotalWeight() {
        // Order knows how to calculate its own weight
        return this.items.reduce((total, item) => {
            return total + item.getTotalWeight()
        }, 0)
    }
    
    private function calculateDistanceFromWarehouse() {
        // Order knows how to calculate distance
        const customerLocation = this.customer.getShippingLocation()
        return customerLocation.distanceFrom(warehouse.getLocation())
    }
}

// Customer provides focused interface
class Customer {
    function getShippingAddress() {
        // Customer knows its own address structure
        return this.profile.primaryAddress
    }
    
    function getShippingLocation() {
        // Customer provides location in format needed for calculations
        return this.profile.primaryAddress.getCoordinates()
    }
}

// OrderItem provides focused interface
class OrderItem {
    function getTotalWeight() {
        // Item knows how to calculate its total weight
        return this.product.weight * this.quantity
    }
}

AI Prompt Example:

"Refactor this code to follow the Law of Demeter. Remove long method chains and instead create methods that provide exactly what's needed without exposing internal object structure."

Designing Systems AI Can Understand and Extend

"Create architectures that AI assistants can reason about and enhance"

When designing for AI collaboration, create clear patterns and consistent structures that your AI familiar can recognize and extend.

✅ AI-Friendly System Architecture

// GOOD: Clear, consistent patterns AI can understand and extend

// 1. Consistent naming conventions
interface UserService {
    function createUser(userData)
    function updateUser(userId, userData)
    function deleteUser(userId)
    function getUser(userId)
    function listUsers(criteria)
}

interface ProductService {
    function createProduct(productData)
    function updateProduct(productId, productData)
    function deleteProduct(productId)
    function getProduct(productId)
    function listProducts(criteria)
}

// 2. Standardized error handling
class ServiceResult {
    constructor(success, data, error) {
        this.success = success
        this.data = data
        this.error = error
    }
    
    static success(data) {
        return new ServiceResult(true, data, null)
    }
    
    static failure(error) {
        return new ServiceResult(false, null, error)
    }
}

// 3. Predictable service patterns
class BaseService {
    constructor(repository, validator, logger) {
        this.repository = repository
        this.validator = validator
        this.logger = logger
    }
    
    function create(data) {
        // Consistent pattern across all services
        const validationResult = this.validator.validate(data)
        if (!validationResult.isValid) {
            return ServiceResult.failure(validationResult.errors)
        }
        
        try {
            const entity = this.repository.save(data)
            this.logger.info(`Created ${this.entityName}: ${entity.id}`)
            return ServiceResult.success(entity)
        } catch (error) {
            this.logger.error(`Failed to create ${this.entityName}: ${error.message}`)
            return ServiceResult.failure(error.message)
        }
    }
}

// 4. Clear dependency patterns
class ServiceFactory {
    static createUserService() {
        return new UserService(
            new UserRepository(),
            new UserValidator(),
            new Logger('UserService')
        )
    }
    
    static createProductService() {
        return new ProductService(
            new ProductRepository(),
            new ProductValidator(),
            new Logger('ProductService')
        )
    }
}

AI Prompt Example:

"Create a service architecture that follows consistent patterns. Each service should have the same interface structure (create, update, delete, get, list), use standardized error handling, and follow predictable dependency injection patterns."

Building AI-Collaborative Systems

When working with your AI familiar on system design:

For Composition over Inheritance:

"Use composition instead of inheritance. Create capability interfaces that can be combined flexibly to build different types of objects."

For Dependency Injection:

"Remove direct dependencies and use dependency injection. Create interfaces for dependencies and inject them through constructors."

For Tell, Don't Ask:

"Refactor this to use Tell, Don't Ask. Instead of checking object state and making decisions, tell objects what you want them to do."

For Law of Demeter:

"Fix these method chains. Create methods that provide exactly what's needed without exposing internal object relationships."

For AI-Friendly Design:

"Create consistent patterns that follow the same structure across all similar components. Use standardized naming, error handling, and dependency patterns."

The Symphony of System Design

When you combine these principles, you create systems that are:

  • Flexible: Composition allows easy recombination of capabilities
  • Testable: Dependency injection enables isolated testing
  • Maintainable: Tell, Don't Ask keeps responsibilities clear
  • Resilient: Law of Demeter reduces coupling and cascading changes
  • Extensible: Consistent patterns make it easy to add new components

Your Next Magical Steps

These system design principles create the foundation for scalable, maintainable architectures. In our next scroll, we'll explore Error Handling & Defensive Programming - the protective enchantments that keep your systems running smoothly even when unexpected magic occurs.

Remember: Great architecture is like a well-planned city - it grows gracefully, adapts to changing needs, and makes life better for all its inhabitants.

Until next time, may your compositions be flexible and your dependencies well-injected!


This scroll is part of our Vibe Coding Principles series, exploring how fundamental software principles enhance AI-assisted development.

Share this post