Skip to content

Architecture Principles

Spinning Idea edited this page May 27, 2026 · 2 revisions

Software Architecture & Design Principles

This document outlines the high-level principles of software architecture, design, and coding practices that an engineering team could follow. Aligning on these principles ensures our systems remain modular, maintainable, highly testable, and capable of adapting to changing requirements with minimal friction.


Table of Contents

  1. Core Design Philosophies
  2. Object-Oriented & Modular Design
  3. Codebase Organization & Health
  4. Modern Cloud-Native Delivery

1. Core Design Philosophies

KISS (Keep It Simple, Stupid)

Simplicity is the ultimate sophistication. Complexity is the enemy of reliability and speed.

  • The Philosophy: Always build the simplest solution that satisfies the current functional requirements. Avoid clever tricks, deep meta-programming, or complex multi-layer patterns when standard, readable, straight-forward code does the job.
  • In JavaScript/TypeScript: Avoid writing complex custom runtime type validators or highly abstract state-management wrappers before vanilla functions or React hooks prove insufficient. Keep component states local and flat unless shared state is absolutely required.
  • In Python: Embrace the Zen of Python: "Explicit is better than implicit." and "Simple is better than complex." Avoid writing highly custom metaclasses, massive decorator chains, or overly abstract generic classes that make tracing runtime behavior difficult.
  • Deep-Dive Resources:

DRY (Don't Repeat Yourself) / AHA (Avoid Hasty Abstractions)

Every piece of knowledge or business logic must have a single, unambiguous, authoritative representation in the codebase.

  • The Philosophy: Avoid copy-pasting code that represents the same piece of business logic. However, do not over-apply DRY. Duplicating structurally similar code that represents different business concepts is often better than creating a fragile, premature abstraction. Keep in mind Sandi Metz's insight: "Duplication is far cheaper than the wrong abstraction."
  • In JavaScript/TypeScript: Abstract common rendering UI elements into dedicated reusable components. Pull shared business rules out of UI components and into reusable utility functions or custom hooks.
  • In Python: Consolidate repetitive data modeling validation, date conversions, or authorization checks into shared functions or decorators, keeping them modular and highly unit-tested.
  • Deep-Dive Resources:

YAGNI (You Aren't Gonna Need It)

Implement features and abstractions ONLY when you need them, never when you merely foresee that you might.

  • The Philosophy: Premature optimization and speculative development waste valuable engineering time, bloat the codebase, and frequently miss the mark when actual requirements finally materialize. Only build for what you know to be true today.
  • In JavaScript/TypeScript: Do not set up complex, generic multi-tenant framework configurations or custom UI state caching systems until multi-tenancy or severe caching performance bottlenecks actually exist.
  • In Python: Do not write complex base repository wrappers for a database "just in case we switch from PostgreSQL to MongoDB." Build the simple, direct queries needed for today's database.
  • Deep-Dive Resources:

Composition over Inheritance

Combine simple, focused classes, objects, or functions to create rich behavior, rather than relying on deep class inheritance hierarchies.

  • The Philosophy: Inheritance creates tight coupling. A change in a parent class can break subclasses down the hierarchy (the fragile base class problem). Composition provides loose coupling, making behavior far easier to swap or mock.
  • In JavaScript/TypeScript: In React, favor nesting components via props (children) or consuming logic through focused Custom Hooks rather than extending base classes.
  • In Python: Instead of creating a deep subclass structure for database-interacting handlers, inject the database client or repository into a standard class, or use Python Mixins / Protocols for dynamic polymorphic behaviors.
  • Deep-Dive Resources:

2. Object-Oriented & Modular Design

SOLID Principles

The foundational building blocks of robust, object-oriented and modular system design.

  • Single Responsibility Principle (SRP): A module, class, or function should have one, and only one, reason to change. Separate business logic from rendering, and separate data retrieval from data mutation.
  • Open/Closed Principle (OCP): Software entities should be open for extension, but closed for modification. You should be able to introduce new features by adding new code, not rewriting existing, working code.
  • Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types without altering the correctness of the program.
  • Interface Segregation Principle (ISP): Clients should not be forced to depend on methods they do not use. Prefer many small, client-specific interfaces over one large, general-purpose interface.
  • Dependency Inversion Principle (DIP): Depend on abstractions (interfaces, protocols), not concrete implementations. High-level modules should not depend on low-level modules; both should depend on abstractions.
  • Deep-Dive Resources:

Ports & Adapters (Hexagonal Architecture)

Decouple the core business logic of your application from external infrastructures (such as databases, user interfaces, queues, and external APIs).

                     +----------------------------+
                     |        Infrastructure      |
                     |  [Adapters: Web API, CLI]  |
                     +-------------+--------------+
                                   | (drives)
                                   v
                     +----------------------------+
                     |           Ports            |
                     |  [Abstract Interfaces]     |
                     +-------------+--------------+
                                   |
                                   v
                     +----------------------------+
                     |         Core Domain        |
                     |  [Pure Business Logic]     |
                     +-------------+--------------+
                                   ^
                                   | (implements)
                     +-------------+--------------+
                     |        Infrastructure      |
                     |  [Adapters: Postgres, S3]  |
                     +----------------------------+
  • The Philosophy: Core business logic is the most valuable and stable part of your software. By wrapping the domain in "Ports" (abstract interfaces representing capabilities, e.g., UserRepository, PaymentProcessor) and mapping external systems via "Adapters" (concrete implementations, e.g., PostgreSQLUserRepository, StripePaymentProcessor), the core domain remains isolated, highly testable, and immune to framework or database changes.
  • In JavaScript/TypeScript: Use TypeScript interface keyword to define ports. Inject these interfaces into your services, and instantiate concrete adapters at the system boundary (e.g., in a bootstrap or dependency injection module).
  • In Python: Define ports using Abstract Base Classes (abc.ABC) or typing.Protocol to enforce structural typing. Concrete implementations subclass the ABCs and are passed to domain services at runtime.
  • Deep-Dive Resources:

Domain-Driven Design (DDD) Lite

Align your codebase with the real-world business context and vocabulary (Ubiquitous Language).

  • The Philosophy: Focus on capturing domain rules cleanly. Avoid leaking framework details (such as database schemas, ORM objects like SQLAlchemy models or Mongoose models, or HTTP request parameters) into core business functions.
  • Entities & Value Objects: Entities have a distinct identity that persists over time (e.g., a User with a unique ID). Value Objects have no identity and are defined entirely by their attributes; they are immutable (e.g., an Address or a Money object representing amount and currency).
  • In JavaScript/TypeScript: Model domain logic with pure TypeScript classes or immutable objects, and validate schemas using toolkits like Zod.
  • In Python: Leverage Python dataclasses (preferably with frozen=True) or Pydantic models to implement type-safe, immutable Value Objects and Entities.
  • Deep-Dive Resources:

3. Codebase Organization & Health

Screaming Architecture & Feature-First Folder Structure

Your physical folder organization should clearly reveal the domain capabilities of the application, not the web framework in use.

  • The Philosophy: Do not group files technically (e.g., placing all controllers in /controllers, all services in /services, and all templates in /templates). Instead, group files by business capabilities (features). If a feature is deprecated, you can delete a single folder and clean up almost all of its related code instantly.
  • Modern JavaScript/TypeScript (e.g., React/Next.js): Adopt a feature-oriented layout, often referred to as "colocating assets":
    src/
    ├── components/         # Shared global UI elements (Button, Card, Input)
    ├── hooks/              # Shared global React hooks
    ├── features/           # Feature-first modules
    │   ├── user-billing/
    │   │   ├── components/ # Private UI components specific to billing
    │   │   ├── api/        # Fetching functions for billing
    │   │   ├── hooks/      # Local hooks for billing logic
    │   │   └── BillingPage.tsx
    │   └── auth/
    │       ├── components/
    │       └── useAuth.ts
    
  • Modern Python: Structure projects using the src/ layout to prevent runtime path resolution errors and maintain modular package boundaries:
    src/
    └── my_app/
        ├── core/           # Configuration, database setup, exceptions
        ├── domain/         # Pure domain entities, value objects, ports
        └── services/       # Feature/Use-case services (e.g., billing, authentication)
    
  • Deep-Dive Resources:

Code Conventions & Automated Quality Gateways

Code conventions must be strictly defined and automatically enforced by tooling to completely eliminate style debates in Pull Requests.

  • The Philosophy: Consistent formatting makes a codebase feel written by a single, cohesive team. Linting and formatting should happen on save or inside local git pre-commit hooks, guaranteeing that any code pushed to CI is already clean.
  • For JavaScript/TypeScript:
    • Standardize on ESLint and Prettier.
    • Enable strict TypeScript compiler flags (strict: true) to ensure type safety.
    • Follow the Airbnb JavaScript Style Guide or standard community conventions.
  • For Python:
    • Enforce formatting and linting via Ruff (a blazingly fast Linter/Formatter replacing flake8, black, isort, and bandit all at once).
    • Add static typing parameters using type hints and validate with mypy or pyright.
    • Adhere strictly to PEP 8 - Style Guide for Python Code.
  • Deep-Dive Resources:

4. Modern Cloud-Native Delivery

The Twelve-Factor App Methodology

A set of best practices for building scalable, resilient, cloud-native applications.

  • Key Factors to Practice:
    • III. Config: Store configuration and secrets in environment variables (process.env in Node, os.environ or Pydantic-Settings in Python). Never commit raw API keys, db passwords, or environment-specific hosts to version control.
    • VI. Processes: Run application processes as stateless and share-nothing. State (like user sessions or temporary uploads) must be stored in backing stores (e.g., Redis, PostgreSQL, S3).
    • X. Dev/Prod Parity: Keep development, staging, and production as similar as possible to catch issues early. Containerize services (using Docker/Compose) so that the local machine environment exactly mirrors production.
  • Deep-Dive Resources:

Testability & The Test-First Mindset

High-quality test suites act as living documentation and empower teams to refactor with absolute confidence.

  • The Philosophy: If code is extremely difficult to test, it is almost always poorly designed. High testability is a natural side-effect of applying SOLID, KISS, and Ports & Adapters. Align test strategy around the Testing Pyramid:
              /\
             /  \      E2E Tests (Playwright / Cypress) - Low quantity, high confidence.
            /----\
           /      \    Integration/Service Tests - API endpoints, database state logic.
          /--------\
         /          \  Unit Tests (pytest / Vitest) - Pure functions, domain models. High quantity, fast.
        /------------\
    
  • In JavaScript/TypeScript: Use Vitest or Jest for lightning-fast unit and component isolation tests. Use Playwright for robust browser-driven end-to-end regression verification.
  • In Python: Standarize on pytest. Leverage its fixture dependency injection system to manage test databases and mock external APIs cleanly.
  • Deep-Dive Resources:

Clone this wiki locally