Overview
Coupling measures the degree of interdependence between software modules, while cohesion measures how closely related the responsibilities within a single module are. These two metrics form the foundation of modular design, directly impacting maintainability, testability, and system flexibility.
Larry Constantine introduced these concepts in the late 1960s as part of structured design methodology. Coupling describes the strength of connections between modules, ranging from tight coupling where changes in one module force changes in others, to loose coupling where modules operate independently. Cohesion describes the internal unity of a module, ranging from low cohesion where a module handles unrelated responsibilities, to high cohesion where all elements work toward a single, well-defined purpose.
The relationship between these metrics drives design quality: systems with low coupling and high cohesion resist change propagation, isolate failures, and support independent development and testing. Conversely, tightly coupled systems with low cohesion create cascading failures, complicate testing, and make modifications risky.
# High coupling - Order directly manipulates Customer internals
class Order
def apply_discount(customer)
if customer.purchase_history.total > 1000
customer.loyalty_points += 50
customer.discount_tier = 'gold'
end
@total *= 0.9
end
end
# Low coupling - Order delegates through Customer's interface
class Order
def apply_discount(customer)
customer.process_loyalty_reward(50) if qualifies_for_reward?(customer)
@total *= 0.9
end
private
def qualifies_for_reward?(customer)
customer.lifetime_value > 1000
end
end
The distinction matters because coupling and cohesion interact: reducing coupling often requires increasing cohesion, as responsibilities consolidate into well-defined modules. This tension forces architectural decisions about where to draw module boundaries.
Key Principles
Coupling exists in multiple forms, each with different implications for system design. Content coupling occurs when one module modifies or relies on the internal workings of another. Common coupling emerges when multiple modules share global data. Control coupling appears when one module controls the flow of another by passing control information. Stamp coupling happens when modules share composite data structures but use only portions of them. Data coupling, the loosest form, involves modules sharing only primitive data through parameters.
# Content coupling - accessing another class's internals
class ReportGenerator
def generate(user)
# Directly accessing and modifying User's internal state
user.instance_variable_set(:@last_report_date, Time.now)
data = user.instance_variable_get(:@cached_data)
end
end
# Data coupling - clean interface communication
class ReportGenerator
def generate(user_id, report_type)
user_data = UserRepository.fetch(user_id)
format_report(user_data, report_type)
end
end
Cohesion also manifests in distinct levels. Coincidental cohesion represents the weakest form, where module elements share no meaningful relationship. Logical cohesion groups elements that perform similar operations but on different data. Temporal cohesion clusters operations performed at the same time. Procedural cohesion links elements that execute in sequence. Communicational cohesion groups operations on the same data. Sequential cohesion chains elements where output from one feeds into the next. Functional cohesion, the strongest form, unifies all elements toward a single, well-defined purpose.
# Low cohesion - mixed responsibilities
class UserManager
def create_user(params)
# User creation
end
def send_email(to, subject)
# Email sending
end
def log_activity(action)
# Logging
end
def validate_credit_card(number)
# Payment validation
end
end
# High cohesion - focused responsibility
class UserRegistration
def initialize(params)
@params = params
end
def register
validate_input
create_user_record
send_welcome_email
end
private
def validate_input
# Validation logic
end
def create_user_record
# Persistence logic
end
def send_welcome_email
# Welcome email logic
end
end
The dependency inversion principle relates directly to coupling: high-level modules should not depend on low-level modules, both should depend on abstractions. This principle reduces coupling by introducing interfaces that isolate modules from implementation details. Ruby's duck typing supports this through implicit interfaces, where objects need only respond to expected methods rather than inherit from specific types.
Information hiding reinforces low coupling by restricting access to module internals. Ruby provides varying degrees of visibility through public, protected, and private methods, though the language's dynamic nature allows circumvention of these restrictions. The principle remains valuable: expose minimal interfaces and encapsulate implementation details.
The Law of Demeter codifies low coupling through a simple rule: an object should only talk to its immediate neighbors. Specifically, a method should only call methods on: the object itself, method parameters, objects it creates, and its direct attributes. Violations create coupling chains where changes ripple through multiple modules.
# Violates Law of Demeter
class OrderProcessor
def calculate_shipping(order)
order.customer.address.country.shipping_rate
end
end
# Follows Law of Demeter
class OrderProcessor
def calculate_shipping(order)
order.shipping_rate
end
end
class Order
def shipping_rate
customer.shipping_rate_for_location
end
end
Ruby Implementation
Ruby's module system provides several mechanisms for managing coupling and cohesion. Modules serve as namespaces, mixins, and containers for related functionality. The include keyword adds module methods as instance methods, extend adds them as class methods, and prepend inserts the module before the class in the method lookup chain.
module Auditable
def audit_log
@audit_log ||= []
end
def record_change(attribute, old_value, new_value)
audit_log << {
attribute: attribute,
old_value: old_value,
new_value: new_value,
timestamp: Time.now
}
end
end
class Account
include Auditable
attr_reader :balance
def initialize(initial_balance)
@balance = initial_balance
end
def deposit(amount)
old_balance = @balance
@balance += amount
record_change(:balance, old_balance, @balance)
end
end
Ruby's open classes create potential for tight coupling. Any code can modify any class at runtime, leading to action-at-a-distance problems where changes in one location affect distant code. Refinements provide scoped monkey patching, limiting modifications to specific contexts.
# Global modification - high coupling risk
class String
def titleize
split.map(&:capitalize).join(' ')
end
end
# Scoped modification - controlled coupling
module StringRefinements
refine String do
def titleize
split.map(&:capitalize).join(' ')
end
end
end
class DocumentFormatter
using StringRefinements
def format_title(text)
text.titleize
end
end
Dependency injection reduces coupling by providing dependencies from outside rather than hardcoding them internally. Ruby's dynamic nature makes this straightforward through constructor injection, setter injection, or parameter injection.
# Tight coupling - hardcoded dependency
class OrderNotifier
def notify(order)
SmtpMailer.new.send(
to: order.customer.email,
subject: "Order Confirmation",
body: format_order(order)
)
end
end
# Loose coupling - injected dependency
class OrderNotifier
def initialize(mailer: SmtpMailer.new)
@mailer = mailer
end
def notify(order)
@mailer.send(
to: order.customer.email,
subject: "Order Confirmation",
body: format_order(order)
)
end
end
Ruby's blocks, procs, and lambdas enable strategy pattern implementations that reduce coupling by abstracting algorithms. Objects can accept behavior as parameters, avoiding tight coupling to specific implementations.
class DataProcessor
def initialize(validator:, transformer:)
@validator = validator
@transformer = transformer
end
def process(data)
return [] unless @validator.call(data)
data.map(&@transformer)
end
end
processor = DataProcessor.new(
validator: ->(data) { data.is_a?(Array) && !data.empty? },
transformer: ->(item) { item.to_s.upcase }
)
The single responsibility principle guides cohesion in Ruby classes. Each class should have one reason to change, meaning it addresses one specific concern. Ruby's expressive syntax supports this through clear method names and focused classes.
# Low cohesion - multiple responsibilities
class UserAccount
def authenticate(password)
# Authentication logic
end
def generate_pdf_report
# PDF generation
end
def send_sms(message)
# SMS sending
end
end
# High cohesion - separated responsibilities
class Authenticator
def authenticate(user, password)
# Authentication logic
end
end
class ReportGenerator
def generate_pdf(user)
# PDF generation
end
end
class NotificationService
def send_sms(recipient, message)
# SMS sending
end
end
Practical Examples
Consider an e-commerce system handling order processing. A tightly coupled design creates dependencies between orders, inventory, shipping, and payment systems.
# Tightly coupled implementation
class Order
def complete
# Direct database access
inventory = ActiveRecord::Base.connection.execute(
"SELECT * FROM inventory WHERE product_id = #{@product_id}"
)
# Direct manipulation of other classes' internals
@product.inventory_count -= @quantity
@product.save!
# Hardcoded payment processing
stripe_charge = Stripe::Charge.create(
amount: @total * 100,
currency: 'usd',
source: @customer.payment_token
)
# Direct shipping API call
shipping_response = HTTP.post(
'https://shipping-api.example.com/create',
json: {
address: @customer.address,
weight: @product.weight
}
)
@status = 'completed'
save!
end
end
This design exhibits multiple coupling problems: content coupling through direct database access, common coupling through shared global state, and control coupling through hardcoded API interactions. Changes to payment processing, shipping providers, or inventory management require modifying the Order class.
A loosely coupled alternative isolates concerns through interfaces:
# Loosely coupled implementation
class OrderCompletionService
def initialize(
inventory_service:,
payment_service:,
shipping_service:,
notification_service:
)
@inventory_service = inventory_service
@payment_service = payment_service
@shipping_service = shipping_service
@notification_service = notification_service
end
def complete(order)
return failure('Insufficient inventory') unless @inventory_service.reserve(
product_id: order.product_id,
quantity: order.quantity
)
payment_result = @payment_service.charge(
customer_id: order.customer_id,
amount: order.total
)
return failure('Payment failed') unless payment_result.success?
shipping_result = @shipping_service.schedule(
order_id: order.id,
address: order.shipping_address
)
order.complete!(
payment_id: payment_result.id,
shipping_id: shipping_result.id
)
@notification_service.order_confirmed(order)
success(order)
end
private
def success(order)
Result.new(success: true, order: order)
end
def failure(reason)
Result.new(success: false, error: reason)
end
end
Each service handles a cohesive set of responsibilities. The inventory service manages stock levels, the payment service processes transactions, and the shipping service coordinates delivery. Order completion orchestrates these services without knowing their internal implementations.
Authentication and authorization demonstrate cohesion principles. A low-cohesion approach mixes concerns:
# Low cohesion - mixed concerns
class UserController
def login
user = User.find_by(email: params[:email])
if user && BCrypt::Password.new(user.password_hash) == params[:password]
session[:user_id] = user.id
session[:login_time] = Time.now
# Logging mixed with authentication
Rails.logger.info "User #{user.id} logged in"
# Email notification mixed with authentication
UserMailer.login_notification(user).deliver_later
# Permission checking mixed with authentication
if user.admin?
session[:admin] = true
end
redirect_to dashboard_path
else
# Analytics mixed with authentication
Analytics.track('failed_login', email: params[:email])
flash[:error] = 'Invalid credentials'
render :login
end
end
end
Separating these concerns improves cohesion:
# High cohesion - separated concerns
class SessionsController
def create
result = AuthenticationService.authenticate(
email: params[:email],
password: params[:password]
)
if result.success?
SessionManager.create_session(result.user, session)
redirect_to dashboard_path
else
flash[:error] = 'Invalid credentials'
render :new
end
end
end
class AuthenticationService
def self.authenticate(email:, password:)
user = User.find_by(email: email)
return failure unless user && user.authenticate(password)
notify_successful_login(user)
success(user)
end
private
def self.notify_successful_login(user)
LoginNotificationJob.perform_later(user.id)
LoginLogger.log(user)
LoginAnalytics.track(user)
end
def self.success(user)
Result.new(success: true, user: user)
end
def self.failure
Result.new(success: false, user: nil)
end
end
class SessionManager
def self.create_session(user, session)
session[:user_id] = user.id
session[:login_time] = Time.now
session[:permissions] = PermissionResolver.resolve(user)
end
end
Design Considerations
Coupling and cohesion tradeoffs affect design decisions throughout development. Reducing coupling often requires adding abstractions, which increases code complexity. The decision depends on change frequency and testing requirements.
Direct coupling minimizes abstraction overhead but creates rigid systems:
class ReportExporter
def export(report, format)
case format
when 'pdf'
PdfGenerator.new.generate(report)
when 'csv'
CsvGenerator.new.generate(report)
when 'excel'
ExcelGenerator.new.generate(report)
end
end
end
This approach works when formats rarely change and generators share no testing concerns. Adding formats requires modifying ReportExporter, but the simplicity benefits small, stable systems.
Interface-based coupling adds flexibility at the cost of indirection:
class ReportExporter
def initialize(generators: {})
@generators = generators
end
def export(report, format)
generator = @generators[format]
raise UnknownFormat, format unless generator
generator.generate(report)
end
end
# Configuration
exporter = ReportExporter.new(
generators: {
'pdf' => PdfGenerator.new,
'csv' => CsvGenerator.new,
'excel' => ExcelGenerator.new
}
)
This design supports testing with mock generators and adding formats without modifying ReportExporter. The tradeoff: more moving parts and configuration complexity.
Cohesion decisions balance granularity against coordination overhead. Fine-grained classes with high cohesion create more objects to coordinate:
class OrderProcessor
def initialize
@validator = OrderValidator.new
@inventory = InventoryChecker.new
@pricer = PriceCalculator.new
@persister = OrderPersister.new
end
def process(order_params)
errors = @validator.validate(order_params)
return failure(errors) unless errors.empty?
return failure('Out of stock') unless @inventory.available?(order_params)
order = build_order(order_params)
order.total = @pricer.calculate(order)
@persister.save(order)
success(order)
end
end
Coarser-grained classes reduce coordination but risk mixed responsibilities:
class OrderProcessor
def process(order_params)
return failure('Invalid order') unless valid?(order_params)
return failure('Out of stock') unless available?(order_params)
order = create_order(order_params)
calculate_total(order)
save_order(order)
success(order)
end
private
def valid?(params)
# Validation logic
end
def available?(params)
# Inventory logic
end
def create_order(params)
# Creation logic
end
def calculate_total(order)
# Pricing logic
end
def save_order(order)
# Persistence logic
end
end
The choice depends on reuse requirements, testing strategies, and team structure. Systems with multiple order types benefit from fine-grained separation, while simple CRUD applications favor consolidation.
Module boundaries affect coupling propagation. Placing related classes in the same module reduces coupling distances but increases module coupling. Spreading classes across modules creates loose coupling between modules but complicates related changes.
# Single module - tight internal coupling, loose external coupling
module OrderManagement
class Order
def calculate_shipping
ShippingCalculator.calculate(self)
end
end
class ShippingCalculator
def self.calculate(order)
# Calculation logic with direct Order access
end
end
class InventoryChecker
def self.check(order)
# Checking logic with direct Order access
end
end
end
# Separate modules - loose internal coupling, more coordination
module Orders
class Order
def calculate_shipping
Shipping::Calculator.calculate(shipping_params)
end
private
def shipping_params
{
weight: total_weight,
destination: shipping_address,
method: shipping_method
}
end
end
end
module Shipping
class Calculator
def self.calculate(params)
# Calculation logic using only params
end
end
end
Common Patterns
The facade pattern reduces coupling by providing a simplified interface to complex subsystems. Clients interact with the facade rather than multiple subsystem classes.
class PaymentFacade
def initialize
@validator = PaymentValidator.new
@processor = PaymentProcessor.new
@notifier = PaymentNotifier.new
@logger = PaymentLogger.new
end
def process_payment(amount:, source:, customer:)
validation = @validator.validate(amount, source)
return failure(validation.errors) unless validation.valid?
result = @processor.charge(amount: amount, source: source)
if result.success?
@notifier.payment_succeeded(customer, amount)
@logger.log_success(result.transaction_id)
success(result)
else
@notifier.payment_failed(customer, result.reason)
@logger.log_failure(result.error)
failure(result.reason)
end
end
end
The adapter pattern decouples clients from specific implementations by wrapping incompatible interfaces:
# External service with incompatible interface
class ThirdPartyShipping
def create_shipment(data)
# Expects specific data structure
end
end
# Adapter providing application interface
class ShippingAdapter
def initialize(service = ThirdPartyShipping.new)
@service = service
end
def ship(order)
shipment_data = {
origin: format_address(order.origin_address),
destination: format_address(order.destination_address),
packages: format_packages(order.items),
service_level: map_service_level(order.shipping_speed)
}
@service.create_shipment(shipment_data)
end
private
def format_address(address)
# Transform application address to service format
end
def format_packages(items)
# Transform items to package format
end
def map_service_level(speed)
# Map application speed to service levels
end
end
The mediator pattern reduces coupling between components by centralizing communication:
class ChatRoom
def initialize
@participants = []
end
def join(participant)
@participants << participant
participant.chat_room = self
end
def send_message(sender, message)
@participants.each do |participant|
participant.receive(message) unless participant == sender
end
end
end
class Participant
attr_accessor :chat_room
attr_reader :name
def initialize(name)
@name = name
@messages = []
end
def send(message)
@chat_room.send_message(self, message)
end
def receive(message)
@messages << message
end
end
The observer pattern decouples subjects from observers, allowing dynamic subscription:
class OrderSubject
def initialize
@observers = []
end
def attach(observer)
@observers << observer
end
def detach(observer)
@observers.delete(observer)
end
def notify(event, data)
@observers.each { |observer| observer.update(event, data) }
end
end
class Order < OrderSubject
def complete
# Order completion logic
notify(:order_completed, self)
end
end
class InventoryObserver
def update(event, order)
return unless event == :order_completed
InventoryService.reduce_stock(order.items)
end
end
class EmailObserver
def update(event, order)
return unless event == :order_completed
OrderMailer.confirmation(order).deliver_later
end
end
class AnalyticsObserver
def update(event, order)
return unless event == :order_completed
Analytics.track('order_completed', order_id: order.id)
end
end
Dependency injection containers manage object creation and wiring, reducing configuration coupling:
class Container
def initialize
@services = {}
@factories = {}
end
def register(name, &factory)
@factories[name] = factory
end
def get(name)
@services[name] ||= @factories[name].call(self)
end
end
# Configuration
container = Container.new
container.register(:database) do
Database.connect(ENV['DATABASE_URL'])
end
container.register(:user_repository) do |c|
UserRepository.new(database: c.get(:database))
end
container.register(:authentication_service) do |c|
AuthenticationService.new(
repository: c.get(:user_repository),
encryptor: c.get(:encryptor)
)
end
Common Pitfalls
Premature abstraction creates unnecessary coupling to abstractions that provide no value. Adding interfaces before variation exists complicates code without benefit.
# Premature abstraction
class UserRepositoryInterface
def find(id)
raise NotImplementedError
end
def save(user)
raise NotImplementedError
end
end
class DatabaseUserRepository < UserRepositoryInterface
def find(id)
User.find(id)
end
def save(user)
user.save!
end
end
# Simpler approach until variation appears
class UserRepository
def find(id)
User.find(id)
end
def save(user)
user.save!
end
end
Callback coupling creates hidden dependencies through lifecycle hooks. Objects register callbacks with other objects, creating implicit coupling that complicates tracing.
# Callback coupling
class Order
attr_accessor :status
def initialize
@callbacks = Hash.new { |h, k| h[k] = [] }
end
def on_status_change(&block)
@callbacks[:status_change] << block
end
def status=(new_status)
old_status = @status
@status = new_status
@callbacks[:status_change].each { |cb| cb.call(old_status, new_status) }
end
end
# Hidden dependencies throughout codebase
order.on_status_change do |old, new|
InventoryService.update if new == 'completed'
end
order.on_status_change do |old, new|
EmailService.notify if new == 'shipped'
end
# Explicit dependencies
class OrderStatusHandler
def initialize(inventory:, email:)
@inventory = inventory
@email = email
end
def handle_change(order, old_status, new_status)
@inventory.update if new_status == 'completed'
@email.notify if new_status == 'shipped'
end
end
Shared mutable state creates coupling between components that modify the same data:
# Shared mutable state
class GlobalCache
@@data = {}
def self.set(key, value)
@@data[key] = value
end
def self.get(key)
@@data[key]
end
end
class ServiceA
def process
GlobalCache.set('result', compute)
end
end
class ServiceB
def process
result = GlobalCache.get('result')
transform(result)
end
end
# Explicit dependencies
class ServiceA
def process
compute
end
end
class ServiceB
def process(input)
transform(input)
end
end
class Coordinator
def initialize(service_a:, service_b:)
@service_a = service_a
@service_b = service_b
end
def execute
result_a = @service_a.process
@service_b.process(result_a)
end
end
Temporal coupling occurs when operations must execute in specific sequences due to implicit state dependencies:
# Temporal coupling - order matters
class DocumentProcessor
def process(document)
parse(document)
validate # Depends on parse being called first
transform # Depends on validate being called
save # Depends on transform being called
end
private
def parse(document)
@parsed_document = Parser.parse(document)
end
def validate
raise 'Invalid' unless @parsed_document.valid?
end
def transform
@transformed = Transformer.transform(@parsed_document)
end
def save
Repository.save(@transformed)
end
end
# Explicit dependencies
class DocumentProcessor
def process(document)
parsed = parse(document)
validated = validate(parsed)
transformed = transform(validated)
save(transformed)
end
private
def parse(document)
Parser.parse(document)
end
def validate(parsed_document)
raise 'Invalid' unless parsed_document.valid?
parsed_document
end
def transform(validated_document)
Transformer.transform(validated_document)
end
def save(transformed_document)
Repository.save(transformed_document)
end
end
God objects accumulate responsibilities, creating low cohesion and coupling to many other classes:
# God object
class Application
def start
# Initialization logic
end
def process_request(request)
# Request handling
end
def authenticate_user(credentials)
# Authentication logic
end
def send_email(recipient, message)
# Email sending
end
def generate_report(type)
# Reporting logic
end
def cleanup
# Cleanup logic
end
end
# Separated responsibilities
class Application
def initialize(
request_handler:,
authenticator:,
email_service:,
report_generator:
)
@request_handler = request_handler
@authenticator = authenticator
@email_service = email_service
@report_generator = report_generator
end
def start
# Only initialization logic
end
def process_request(request)
@request_handler.handle(request)
end
end
Reference
Coupling Types
| Type | Description | Impact |
|---|---|---|
| Content | Module modifies or relies on internal workings of another module | Highest coupling, changes ripple through system |
| Common | Modules share global data | High coupling, race conditions, testing difficulties |
| Control | One module controls flow of another by passing control flags | Medium-high coupling, complicates understanding |
| Stamp | Modules share composite data structures, use only portions | Medium coupling, unnecessary dependencies |
| Data | Modules share only primitive data through parameters | Low coupling, preferred form |
| Message | Modules communicate through message passing with no shared state | Lowest coupling, highest flexibility |
Cohesion Types
| Type | Description | Strength |
|---|---|---|
| Coincidental | Elements have no meaningful relationship | Weakest, avoid |
| Logical | Elements perform similar operations on different data | Weak, creates confusion |
| Temporal | Elements execute at the same time | Weak, mixed concerns |
| Procedural | Elements execute in sequence | Medium-weak, processing focus |
| Communicational | Elements operate on same data | Medium, data focus |
| Sequential | Output of one element feeds next | Medium-strong, workflow focus |
| Functional | All elements contribute to single well-defined task | Strongest, preferred |
Metrics and Indicators
| Metric | Measurement | Target |
|---|---|---|
| Afferent Coupling | Number of classes depending on this class | Varies by role, stable classes higher |
| Efferent Coupling | Number of classes this class depends on | Lower is better |
| Instability | Efferent / (Afferent + Efferent) | 0 for stable, 1 for unstable |
| Lack of Cohesion (LCOM) | Number of method pairs not sharing instance variables | Lower is better |
| Cyclomatic Complexity | Number of linearly independent paths | Below 10 per method |
Design Guidelines
| Principle | Application | Benefit |
|---|---|---|
| Depend on abstractions, not concretions | Use interfaces and dependency injection | Enables testing, reduces coupling |
| Law of Demeter | Talk only to immediate neighbors | Prevents coupling chains |
| Single Responsibility | One reason to change per class | Increases cohesion |
| Open/Closed | Open for extension, closed for modification | Reduces modification coupling |
| Interface Segregation | Many client-specific interfaces over one general | Reduces interface coupling |
Ruby-Specific Considerations
| Feature | Coupling Impact | Recommendation |
|---|---|---|
| Open classes | Global modifications create coupling | Use refinements for scoped changes |
| Dynamic method definition | Hidden dependencies, difficult to trace | Document dynamic behavior, prefer explicit methods |
| Global variables | Common coupling across system | Avoid, use dependency injection |
| Class variables | Shared mutable state | Prefer instance variables or constants |
| Modules as mixins | Multiple inheritance creates coupling | Use composition over inheritance where possible |
Refactoring Patterns
| Pattern | Purpose | Application |
|---|---|---|
| Extract Class | Reduce class size, increase cohesion | When class has multiple responsibilities |
| Extract Interface | Reduce coupling to implementation | When multiple implementations exist or needed for testing |
| Replace Conditional with Polymorphism | Reduce control coupling | When conditionals check object types |
| Introduce Parameter Object | Reduce stamp coupling | When methods share multiple parameters |
| Hide Delegate | Reduce coupling chains | When client navigates multiple associations |
| Remove Middle Man | Reduce unnecessary abstraction | When class only delegates to another |