CrackedRuby CrackedRuby

Overview

Separation of Concerns (SoC) organizes code into distinct sections where each section handles a specific aspect of functionality. This principle reduces coupling between different parts of a system, making code easier to understand, test, and modify. A concern represents any piece of functionality or requirement that affects the code, such as business logic, data access, user interface, logging, or error handling.

The concept originated from Edsger Dijkstra's 1974 paper "On the role of scientific thought" where he emphasized separating different aspects of a problem to manage complexity. Modern software development applies this principle across multiple levels: from individual methods and classes to entire system architectures.

Systems that violate SoC exhibit tight coupling between unrelated functionality. A class that handles database operations, business logic, and HTML rendering simultaneously becomes difficult to test because testing one aspect requires setting up infrastructure for all three. Changes to database schema require modifications throughout the class, increasing the risk of introducing bugs in unrelated functionality.

# Violation: Multiple concerns mixed together
class UserReport
  def generate(user_id)
    # Database concern
    conn = PG.connect(dbname: 'myapp')
    result = conn.exec("SELECT * FROM users WHERE id = #{user_id}")
    
    # Business logic concern
    total = calculate_total(result)
    
    # Presentation concern
    html = "<html><body><h1>#{result['name']}</h1>"
    html += "<p>Total: #{total}</p></body></html>"
    
    html
  end
end

SoC addresses three fundamental questions: what does this code do, where should this functionality live, and how do different parts interact. The principle applies to method design, class organization, module structure, and system architecture. A well-separated system contains components with clear boundaries and minimal knowledge of each other's internal implementation.

Key Principles

Single Responsibility forms the foundation of SoC. Each component performs one conceptual task and changes for one reason. A class responsible for user authentication should not also handle email delivery or database connection pooling. When business rules change, only the business logic component requires modification. When the database schema changes, only the data access component needs updates.

Cohesion measures how strongly related the responsibilities within a component are. High cohesion indicates that all methods and data within a component work toward the same goal. A UserValidator class with methods validate_email, validate_password, and validate_age demonstrates high cohesion. Adding a send_welcome_email method reduces cohesion by introducing an unrelated concern.

Coupling describes the degree of interdependence between components. Loose coupling allows components to interact through well-defined interfaces without knowing internal implementation details. A service that calls payment_processor.charge(amount) exhibits loose coupling. A service that directly modifies payment_processor.gateway.connection.last_transaction exhibits tight coupling, making the code fragile and difficult to change.

Abstraction hides implementation details behind stable interfaces. Components depend on abstract concepts rather than concrete implementations. Code that depends on a Logger interface rather than a specific FileLogger class can work with any logging implementation. The interface defines what operations are available; implementations determine how those operations execute.

# Abstraction through interface
class PaymentService
  def initialize(payment_gateway)
    @gateway = payment_gateway
  end
  
  def process(amount)
    @gateway.charge(amount)
  end
end

# Different implementations satisfy the interface
class StripeGateway
  def charge(amount)
    # Stripe-specific implementation
  end
end

class PayPalGateway
  def charge(amount)
    # PayPal-specific implementation
  end
end

Information Hiding restricts access to component internals. Public interfaces expose only the operations other components need; internal state and helper methods remain private. This prevents external code from depending on implementation details that might change. A class might store data in an array internally but expose methods that suggest a different structure, allowing the internal representation to change without affecting clients.

Modularity organizes code into self-contained units with minimal dependencies. Each module encapsulates related functionality and exposes a small, stable interface. Changes within a module do not propagate to other modules unless the interface changes. This isolation enables independent development, testing, and deployment of different system parts.

The principle extends beyond code structure to concern lifecycle and timing. Runtime concerns include logging, error handling, security, and performance monitoring. These cross-cutting concerns affect multiple components but should not be tangled with business logic. Compile-time concerns include code organization, dependency management, and build configuration. Development-time concerns include testing, debugging, and documentation.

Design Considerations

Apply SoC when complexity reaches the point where multiple developers struggle to understand how changes affect the system. Small scripts or prototypes benefit less from strict separation because the overhead of organizing concerns exceeds the maintenance benefits. As systems grow and multiple developers collaborate, clear boundaries become necessary.

Consider the nature of changes the system experiences. Systems with frequent changes to specific areas benefit from isolating those areas. An e-commerce platform might change pricing rules frequently while inventory management remains stable. Separating pricing logic from inventory logic allows pricing changes without risking inventory bugs. Systems with infrequent, global changes benefit less from fine-grained separation.

Team structure influences separation decisions. Conway's Law observes that system design mirrors communication structure. If separate teams handle frontend and backend development, a clear separation between presentation and business logic aligns with team boundaries. If one small team handles all aspects, extremely fine-grained separation might create more coordination overhead than benefit.

Performance requirements sometimes conflict with clean separation. A high-performance system might combine concerns to reduce overhead. An object-relational mapper provides clean separation between business objects and database access but introduces performance costs. Systems with extreme performance requirements might need more coupled code, accepting maintenance costs for speed gains. The decision depends on whether the performance gain justifies the maintenance burden.

# Clean separation with ORM
class OrderService
  def complete_order(order_id)
    order = Order.find(order_id)
    order.status = 'completed'
    order.save
    
    NotificationService.send_confirmation(order)
  end
end

# Performance-optimized with direct SQL
class OptimizedOrderService
  def complete_order(order_id)
    DB.transaction do
      DB.exec("UPDATE orders SET status = 'completed' WHERE id = ?", order_id)
      email = DB.exec("SELECT email FROM users WHERE id = (SELECT user_id FROM orders WHERE id = ?)", order_id).first
      EmailService.send_to(email, :order_confirmation)
    end
  end
end

Testing requirements often drive separation decisions. Code that mixes concerns becomes difficult to test in isolation. A method that validates input, queries a database, and sends an email requires setting up database fixtures and email mocking for every test. Separating these concerns allows testing validation logic with simple unit tests, testing database operations separately, and testing email delivery independently.

The cost of wrong abstractions exceeds the cost of small duplication. Premature separation based on speculative future needs creates unnecessary complexity. When two pieces of code look similar but serve different purposes, forcing them to share an abstraction creates coupling between unrelated concerns. Wait until the separation becomes obviously beneficial before extracting shared code.

Migration strategy affects separation decisions in existing systems. Gradually separating concerns in a legacy codebase requires different approaches than building a new system with clear boundaries from the start. The strangler pattern wraps legacy components with new interfaces, progressively moving functionality into properly separated modules. This incremental approach reduces risk compared to large-scale rewrites.

Ruby Implementation

Ruby provides several mechanisms for implementing SoC at different levels of granularity. The language's object-oriented nature, module system, and metaprogramming capabilities enable various separation strategies.

Classes and Objects represent the fundamental unit of separation. Each class encapsulates related data and behavior, hiding internal state behind method interfaces. Instance variables remain private; methods define the public contract. Ruby's convention of defining attr_reader, attr_writer, and attr_accessor makes explicit which attributes other objects can access.

class Invoice
  attr_reader :items, :created_at
  
  def initialize
    @items = []
    @created_at = Time.now
  end
  
  def add_item(item)
    @items << item
  end
  
  def total
    @items.sum(&:price)
  end
  
  def tax
    total * tax_rate
  end
  
  private
  
  def tax_rate
    0.08
  end
end

Modules provide namespace separation and behavior sharing without inheritance. Modules group related classes and prevent naming conflicts. The module acts as a namespace boundary, clearly indicating which classes belong to the same concern.

module Authentication
  class Session
    def initialize(user)
      @user = user
      @created_at = Time.now
    end
    
    def expired?
      Time.now - @created_at > 3600
    end
  end
  
  class TokenGenerator
    def generate
      SecureRandom.hex(32)
    end
  end
end

module Billing
  class Session
    def initialize(transaction_id)
      @transaction_id = transaction_id
    end
  end
end

Concerns in Rails extract shared behavior into modules that mix into multiple classes. This separates cross-cutting functionality from core class responsibilities. A concern defines both instance and class methods, along with dependencies on other modules.

# app/models/concerns/publishable.rb
module Publishable
  extend ActiveSupport::Concern
  
  included do
    scope :published, -> { where(published: true) }
    validates :published_at, presence: true, if: :published?
  end
  
  def publish
    update(published: true, published_at: Time.current)
  end
  
  def unpublish
    update(published: false)
  end
  
  class_methods do
    def recent_publications
      published.order(published_at: :desc).limit(10)
    end
  end
end

class Article < ApplicationRecord
  include Publishable
end

class BlogPost < ApplicationRecord
  include Publishable
end

Service Objects extract business logic from models and controllers. Each service handles one specific operation, receiving dependencies through initialization rather than reaching out to global state. This pattern separates orchestration logic from domain entities.

class CreateOrder
  def initialize(user, cart, payment_processor)
    @user = user
    @cart = cart
    @payment_processor = payment_processor
  end
  
  def call
    ActiveRecord::Base.transaction do
      order = Order.create!(user: @user, total: @cart.total)
      @cart.items.each { |item| order.line_items.create!(item: item) }
      
      @payment_processor.charge(@user.payment_method, order.total)
      
      order
    end
  end
end

# Usage separates concerns in controller
class OrdersController < ApplicationController
  def create
    service = CreateOrder.new(
      current_user,
      current_cart,
      PaymentProcessor.new
    )
    
    @order = service.call
    redirect_to @order
  rescue PaymentError => e
    flash[:error] = e.message
    render :checkout
  end
end

Repositories separate data access from business logic. The repository provides an interface for querying and persisting objects without exposing database details. Business logic operates on domain objects without knowing whether they come from SQL, NoSQL, or an API.

class UserRepository
  def find(id)
    record = User.find(id)
    map_to_domain(record)
  end
  
  def find_by_email(email)
    record = User.find_by(email: email)
    map_to_domain(record) if record
  end
  
  def save(user)
    record = User.find_or_initialize_by(id: user.id)
    record.update!(
      name: user.name,
      email: user.email
    )
    user.id = record.id
    user
  end
  
  private
  
  def map_to_domain(record)
    DomainUser.new(
      id: record.id,
      name: record.name,
      email: record.email
    )
  end
end

Decorators separate presentation logic from domain models. The decorator wraps a domain object and adds display-specific methods without polluting the domain model with view concerns.

class UserDecorator
  def initialize(user)
    @user = user
  end
  
  def display_name
    "#{@user.first_name} #{@user.last_name}"
  end
  
  def member_since
    @user.created_at.strftime("%B %Y")
  end
  
  def avatar_url
    @user.avatar_url || default_avatar_url
  end
  
  private
  
  def default_avatar_url
    "https://example.com/default-avatar.png"
  end
  
  def method_missing(method, *args, &block)
    @user.send(method, *args, &block)
  end
  
  def respond_to_missing?(method, include_private = false)
    @user.respond_to?(method, include_private) || super
  end
end

# In view
user = UserDecorator.new(current_user)
user.display_name # => "John Doe"
user.email # => delegates to underlying user object

Common Patterns

Layered Architecture organizes code into horizontal layers where each layer provides services to the layer above and uses services from the layer below. Typical layers include presentation, application, domain, and infrastructure. Each layer depends only on layers below it, never on layers above.

# Presentation Layer
class UsersController < ApplicationController
  def create
    result = UserRegistration.new(user_params).execute
    
    if result.success?
      redirect_to result.user
    else
      @errors = result.errors
      render :new
    end
  end
end

# Application Layer
class UserRegistration
  def initialize(params)
    @params = params
  end
  
  def execute
    user = User.new(@params)
    
    if user.valid?
      UserRepository.new.save(user)
      WelcomeEmail.new(user).deliver
      Result.success(user)
    else
      Result.failure(user.errors)
    end
  end
end

# Domain Layer
class User
  attr_accessor :id, :email, :name
  
  def valid?
    email.present? && email.include?('@')
  end
end

# Infrastructure Layer
class UserRepository
  def save(user)
    record = ActiveRecord::Base.connection.execute(
      "INSERT INTO users (email, name) VALUES (?, ?)",
      user.email, user.name
    )
    user.id = record['id']
    user
  end
end

Model-View-Controller (MVC) separates application concerns into data management, user interface, and control flow. The model contains business logic and data access. The view handles presentation. The controller coordinates between model and view, processing user input and selecting appropriate responses.

Rails implements MVC with specific conventions. Models inherit from ActiveRecord, combining domain logic with persistence. Views use ERB templates for HTML generation. Controllers inherit from ActionController, handling HTTP requests and responses. This separation allows changing the user interface without modifying business logic.

Hexagonal Architecture (Ports and Adapters) places business logic at the center, isolated from external concerns. Ports define interfaces the application needs. Adapters implement those interfaces for specific technologies. The application code depends on ports, not adapters, allowing technology changes without modifying core logic.

# Port (interface)
module PaymentGateway
  def charge(amount, payment_method)
    raise NotImplementedError
  end
end

# Core Application
class CheckoutService
  def initialize(payment_gateway)
    @gateway = payment_gateway
  end
  
  def checkout(order)
    @gateway.charge(order.total, order.payment_method)
    order.mark_complete
  end
end

# Adapters
class StripeAdapter
  include PaymentGateway
  
  def charge(amount, payment_method)
    Stripe::Charge.create(
      amount: amount,
      source: payment_method.stripe_token
    )
  end
end

class TestAdapter
  include PaymentGateway
  
  def charge(amount, payment_method)
    # No actual charging in tests
    true
  end
end

# Usage
service = CheckoutService.new(
  Rails.env.production? ? StripeAdapter.new : TestAdapter.new
)

Command Query Responsibility Segregation (CQRS) separates read and write operations into different models. Commands modify state but return no data. Queries return data but cause no side effects. This separation optimizes each concern independently and clarifies which operations change system state.

# Commands (writes)
class CreateUserCommand
  def initialize(params)
    @params = params
  end
  
  def execute
    User.create!(@params)
    nil # Commands return nothing
  end
end

class UpdateUserCommand
  def initialize(user_id, params)
    @user_id = user_id
    @params = params
  end
  
  def execute
    User.find(@user_id).update!(@params)
    nil
  end
end

# Queries (reads)
class UserQuery
  def self.find_by_email(email)
    User.where(email: email).first
  end
  
  def self.active_users
    User.where(active: true).order(:name)
  end
  
  def self.user_statistics
    {
      total: User.count,
      active: User.where(active: true).count,
      inactive: User.where(active: false).count
    }
  end
end

# Usage
CreateUserCommand.new(email: 'test@example.com').execute
users = UserQuery.active_users

Dependency Injection separates object creation from object use. Components receive dependencies through constructor parameters or setter methods rather than creating them directly. This inverts control flow and reduces coupling between components.

# Without DI - tight coupling
class OrderProcessor
  def process(order)
    mailer = OrderMailer.new
    logger = Logger.new(STDOUT)
    
    mailer.send_confirmation(order)
    logger.info("Processed order #{order.id}")
  end
end

# With DI - loose coupling
class OrderProcessor
  def initialize(mailer:, logger:)
    @mailer = mailer
    @logger = logger
  end
  
  def process(order)
    @mailer.send_confirmation(order)
    @logger.info("Processed order #{order.id}")
  end
end

# Composition root
mailer = Rails.env.production? ? OrderMailer.new : TestMailer.new
logger = Logger.new(Rails.root.join('log', 'orders.log'))
processor = OrderProcessor.new(mailer: mailer, logger: logger)

Event-Driven Architecture separates components through asynchronous events. Publishers emit events when significant actions occur. Subscribers listen for relevant events and react accordingly. Components remain unaware of each other; they share only event definitions.

# Event system
module Events
  @subscribers = Hash.new { |h, k| h[k] = [] }
  
  def self.subscribe(event_type, &block)
    @subscribers[event_type] << block
  end
  
  def self.publish(event_type, data)
    @subscribers[event_type].each { |subscriber| subscriber.call(data) }
  end
end

# Publisher
class Order
  def complete
    update(status: 'completed')
    Events.publish(:order_completed, self)
  end
end

# Subscribers
Events.subscribe(:order_completed) do |order|
  OrderMailer.confirmation_email(order).deliver_later
end

Events.subscribe(:order_completed) do |order|
  InventoryService.update_stock(order)
end

Events.subscribe(:order_completed) do |order|
  AnalyticsService.track_conversion(order)
end

Practical Examples

Example: Refactoring a God Class

A monolithic class handles user management, authentication, and notifications. Separating these concerns improves testability and maintainability.

# Before: All concerns mixed
class UserManager
  def create_user(email, password)
    user = User.new(email: email)
    user.password_hash = BCrypt::Password.create(password)
    
    DB.connection.execute(
      "INSERT INTO users (email, password_hash) VALUES (?, ?)",
      user.email, user.password_hash
    )
    
    Mailer.send_email(
      to: email,
      subject: "Welcome!",
      body: "Thanks for signing up"
    )
    
    session[:user_id] = user.id
    
    user
  end
end

# After: Separated concerns
class UserRepository
  def save(user)
    DB.connection.execute(
      "INSERT INTO users (email, password_hash) VALUES (?, ?)",
      user.email, user.password_hash
    )
    user
  end
end

class PasswordService
  def hash(password)
    BCrypt::Password.create(password)
  end
  
  def verify(password, hash)
    BCrypt::Password.new(hash) == password
  end
end

class UserRegistration
  def initialize(user_repo:, password_service:, mailer:, session_manager:)
    @user_repo = user_repo
    @password_service = password_service
    @mailer = mailer
    @session_manager = session_manager
  end
  
  def register(email, password)
    user = User.new(email: email)
    user.password_hash = @password_service.hash(password)
    
    @user_repo.save(user)
    @mailer.send_welcome_email(user)
    @session_manager.login(user)
    
    user
  end
end

Example: Extracting Cross-Cutting Concerns

Logging and error handling appear throughout an application. Moving these to dedicated components removes duplication and centralizes behavior changes.

# Before: Logging scattered throughout
class PaymentService
  def charge(amount)
    puts "Charging $#{amount}"
    result = gateway.charge(amount)
    puts "Charge result: #{result}"
    result
  rescue => e
    puts "Error: #{e.message}"
    raise
  end
end

class OrderService
  def create(items)
    puts "Creating order with #{items.size} items"
    order = Order.create(items: items)
    puts "Created order #{order.id}"
    order
  rescue => e
    puts "Error: #{e.message}"
    raise
  end
end

# After: Centralized logging
class Logger
  def info(message)
    puts "[INFO] #{Time.now}: #{message}"
  end
  
  def error(message, exception)
    puts "[ERROR] #{Time.now}: #{message}"
    puts "  #{exception.class}: #{exception.message}"
    puts exception.backtrace.take(5).map { |line| "  #{line}" }
  end
end

module Loggable
  def with_logging(operation_name)
    logger.info("Starting #{operation_name}")
    result = yield
    logger.info("Completed #{operation_name}")
    result
  rescue => e
    logger.error("Failed #{operation_name}", e)
    raise
  end
  
  private
  
  def logger
    @logger ||= Logger.new
  end
end

class PaymentService
  include Loggable
  
  def charge(amount)
    with_logging("payment charge of $#{amount}") do
      gateway.charge(amount)
    end
  end
end

class OrderService
  include Loggable
  
  def create(items)
    with_logging("order creation with #{items.size} items") do
      Order.create(items: items)
    end
  end
end

Example: Separating Validation from Business Logic

Validation rules often mix with business operations. Extracting validation into dedicated objects clarifies both concerns.

# Before: Validation mixed with logic
class TransferService
  def transfer(from_account, to_account, amount)
    if amount <= 0
      raise "Amount must be positive"
    end
    
    if from_account.balance < amount
      raise "Insufficient funds"
    end
    
    if to_account.frozen?
      raise "Destination account is frozen"
    end
    
    from_account.balance -= amount
    to_account.balance += amount
    
    Transaction.create(
      from: from_account,
      to: to_account,
      amount: amount
    )
  end
end

# After: Separated validation
class TransferValidator
  def initialize(from_account, to_account, amount)
    @from_account = from_account
    @to_account = to_account
    @amount = amount
    @errors = []
  end
  
  def valid?
    validate_amount
    validate_source_balance
    validate_destination_status
    @errors.empty?
  end
  
  attr_reader :errors
  
  private
  
  def validate_amount
    @errors << "Amount must be positive" if @amount <= 0
  end
  
  def validate_source_balance
    @errors << "Insufficient funds" if @from_account.balance < @amount
  end
  
  def validate_destination_status
    @errors << "Destination account is frozen" if @to_account.frozen?
  end
end

class TransferService
  def transfer(from_account, to_account, amount)
    validator = TransferValidator.new(from_account, to_account, amount)
    
    unless validator.valid?
      raise ValidationError, validator.errors.join(', ')
    end
    
    execute_transfer(from_account, to_account, amount)
  end
  
  private
  
  def execute_transfer(from_account, to_account, amount)
    from_account.balance -= amount
    to_account.balance += amount
    
    Transaction.create(
      from: from_account,
      to: to_account,
      amount: amount
    )
  end
end

Example: Separating Configuration from Code

Hard-coded configuration values create coupling between environment-specific settings and application logic. Extracting configuration allows different settings for different environments without code changes.

# Before: Configuration embedded in code
class EmailService
  def send_email(to, subject, body)
    smtp = Net::SMTP.new('smtp.gmail.com', 587)
    smtp.enable_starttls
    smtp.start('mydomain.com', 'user@mydomain.com', 'password123') do |client|
      message = "From: noreply@mydomain.com\nTo: #{to}\nSubject: #{subject}\n\n#{body}"
      client.send_message(message, 'noreply@mydomain.com', to)
    end
  end
end

# After: Configuration separated
class EmailConfig
  def self.load
    new(
      host: ENV.fetch('SMTP_HOST'),
      port: ENV.fetch('SMTP_PORT').to_i,
      domain: ENV.fetch('SMTP_DOMAIN'),
      username: ENV.fetch('SMTP_USERNAME'),
      password: ENV.fetch('SMTP_PASSWORD'),
      from_address: ENV.fetch('SMTP_FROM_ADDRESS')
    )
  end
  
  attr_reader :host, :port, :domain, :username, :password, :from_address
  
  def initialize(host:, port:, domain:, username:, password:, from_address:)
    @host = host
    @port = port
    @domain = domain
    @username = username
    @password = password
    @from_address = from_address
  end
end

class EmailService
  def initialize(config)
    @config = config
  end
  
  def send_email(to, subject, body)
    smtp = Net::SMTP.new(@config.host, @config.port)
    smtp.enable_starttls
    smtp.start(@config.domain, @config.username, @config.password) do |client|
      message = "From: #{@config.from_address}\nTo: #{to}\nSubject: #{subject}\n\n#{body}"
      client.send_message(message, @config.from_address, to)
    end
  end
end

# Usage
config = EmailConfig.load
service = EmailService.new(config)

Reference

Core Concepts

Concept Definition Primary Benefit
Separation of Concerns Organizing code so each section addresses a distinct aspect of functionality Reduces complexity by isolating changes to specific areas
Single Responsibility Each component has one reason to change Improves maintainability and testability
Cohesion Degree to which component elements belong together High cohesion indicates well-separated concerns
Coupling Degree of interdependence between components Low coupling enables independent changes
Abstraction Hiding implementation details behind interfaces Reduces dependencies on specific implementations
Information Hiding Restricting access to component internals Prevents fragile dependencies on internal details

Ruby Implementation Patterns

Pattern Purpose Key Classes/Modules
Service Objects Extract business logic from models and controllers Plain Ruby class with call or execute method
Concerns Share behavior across multiple classes Module with ActiveSupport::Concern
Repositories Separate data access from business logic Plain Ruby class wrapping ActiveRecord
Decorators Add presentation logic without modifying models Plain Ruby class delegating to domain object
Form Objects Handle complex form input and validation Plain Ruby class with ActiveModel::Model
Query Objects Encapsulate complex database queries Plain Ruby class returning relation or array
Policy Objects Centralize authorization logic Plain Ruby class with query methods

Architectural Patterns

Pattern Structure Best For
Layered Architecture Horizontal layers with unidirectional dependencies Enterprise applications with clear tiers
MVC Model, View, Controller with specific responsibilities Web applications with user interfaces
Hexagonal Architecture Core logic surrounded by adapters Applications requiring technology flexibility
CQRS Separate models for reads and writes Systems with different read/write patterns
Event-Driven Components communicate through events Loosely coupled, scalable systems
Microservices Independent services with separate databases Large systems with team autonomy

Separation Indicators

Indicator Good Separation Poor Separation
Testing Can test each concern independently Must set up multiple concerns for any test
Changes Changes affect single component Changes ripple through multiple components
Understanding Each component has clear purpose Purpose unclear without reading entire codebase
Reuse Components usable in different contexts Components tightly bound to specific context
Dependencies Components depend on abstractions Components depend on concrete implementations

Common Mistakes

Mistake Description Solution
Premature Abstraction Creating separation before patterns emerge Wait for duplication to reveal natural boundaries
Anemic Domain Model All logic in services, models only hold data Move business logic into domain objects
God Objects Single class handling multiple unrelated concerns Split into focused, single-purpose classes
Hidden Dependencies Components accessing global state or singletons Make dependencies explicit through injection
Over-Engineering Creating excessive layers for simple problems Match separation granularity to actual complexity
Leaky Abstractions Implementation details visible through interfaces Design interfaces independent of implementation

Decision Framework

Question Guides Toward Guides Away From
Do multiple developers work on this code? More separation Less separation
Does this area change frequently? Isolate changing parts Keep stable parts together
Are there natural boundaries in the domain? Align with domain concepts Arbitrary technical splits
Does this need independent testing? Separate testable units Combine related logic
Will this run in different contexts? Extract reusable components Context-specific implementation
Are there performance constraints? May need tighter coupling Clean separation preferred