CrackedRuby CrackedRuby

Overview

Layered architecture organizes application code into horizontal strata, where each layer serves a specific role in the system. The pattern emerged from early mainframe systems and gained widespread adoption through enterprise application development. Modern frameworks like Ruby on Rails reflect layered thinking, though they adapt the traditional model to web application needs.

The architecture enforces a unidirectional dependency rule: upper layers depend on lower layers, but lower layers remain unaware of upper layers. This constraint creates predictable data flow and reduces coupling between system components. A typical three-tier implementation divides the application into presentation, business logic, and data access layers.

The pattern addresses several software engineering challenges. It provides a clear structure for organizing code as applications grow. It enables teams to work on different layers concurrently without conflicts. It allows replacement of individual layers without rewriting the entire system. The architecture also maps cleanly to physical deployment tiers, where layers run on separate machines for scalability.

# Traditional three-tier structure in Ruby
class UserController
  def create
    user_service = UserService.new
    user = user_service.register(params)
    render json: user
  end
end

class UserService
  def register(attributes)
    user_repository = UserRepository.new
    user_repository.create(attributes)
  end
end

class UserRepository
  def create(attributes)
    User.create(attributes)
  end
end

Key Principles

Separation of Concerns: Each layer handles a distinct aspect of the application. The presentation layer manages user interaction and display formatting. The business logic layer enforces rules and coordinates operations. The data access layer handles persistence and retrieval. This separation prevents entanglement of unrelated responsibilities.

Dependency Direction: Dependencies flow downward through the stack. The presentation layer calls the business logic layer. The business logic layer calls the data access layer. The data access layer interacts with the database. This creates a directed acyclic graph of dependencies that prevents circular references.

Layer Isolation: Changes within a layer should not force changes in other layers, provided the layer interface remains stable. The presentation layer can switch from REST to GraphQL without modifying business logic. The data access layer can migrate from PostgreSQL to MongoDB without affecting the business rules. This isolation reduces the blast radius of changes.

Abstraction Levels: Each layer operates at a different level of abstraction. The presentation layer thinks in terms of HTTP requests and JSON responses. The business logic layer thinks in terms of domain operations and validation rules. The data access layer thinks in terms of queries and transactions. Code at each level speaks its own vocabulary.

Interface Contracts: Layers communicate through defined interfaces rather than concrete implementations. The business logic layer depends on a repository interface, not a specific PostgreSQL repository. This dependency inversion enables testing and allows swapping implementations without modifying clients.

The traditional model includes these layers from top to bottom:

Presentation Layer: Handles all user interaction, input validation for format, and output formatting. In web applications, this includes controllers, views, serializers, and API endpoints. The layer translates HTTP requests into operations the business layer understands and converts domain objects into HTTP responses.

Business Logic Layer: Contains domain rules, workflow coordination, and transaction management. This layer validates business constraints (credit limits, eligibility rules), orchestrates operations across multiple entities, and maintains data consistency. Domain services, use case classes, and business validators live here.

Data Access Layer: Manages persistence, query construction, and transaction handling. This includes repositories, query builders, and ORM mappings. The layer abstracts away database-specific details and provides a domain-oriented interface for data operations.

Database Layer: The actual database management system exists outside the application code but forms the foundation of the stack. Some models include this as a separate layer, others consider it infrastructure supporting the data access layer.

# Layer isolation through interfaces
module Repositories
  class UserRepositoryInterface
    def find(id)
      raise NotImplementedError
    end
    
    def save(user)
      raise NotImplementedError
    end
  end
end

class PostgresUserRepository < Repositories::UserRepositoryInterface
  def find(id)
    User.find(id)
  end
  
  def save(user)
    user.save
  end
end

class BusinessLogicService
  def initialize(user_repository: PostgresUserRepository.new)
    @user_repository = user_repository
  end
  
  def promote_user(user_id)
    user = @user_repository.find(user_id)
    user.role = 'premium'
    @user_repository.save(user)
  end
end

Design Considerations

Layered architecture fits scenarios where clear separation of concerns outweighs the overhead of additional abstractions. Enterprise applications benefit from explicit layers because multiple teams work on different parts of the system simultaneously. Applications with complex business rules benefit because the business logic layer provides a clear location for encoding those rules. Applications that need to support multiple interfaces (web, mobile API, admin panel) benefit because they share the business logic layer while implementing different presentation layers.

The architecture adds indirection that some applications do not need. Simple CRUD applications may find the overhead excessive. Microservices with focused responsibilities may achieve adequate separation without explicit layering. Performance-critical systems may find the layer traversal cost unacceptable.

Rigid vs Relaxed Layering: Strict layered architecture prohibits layers from skipping intermediaries. The presentation layer cannot directly access the database; all requests pass through the business logic layer. Relaxed layering allows upper layers to access any lower layer. The presentation layer might query the database directly for read-only list operations while routing writes through the business logic layer. Relaxed layering improves performance but reduces isolation.

Open vs Closed Layers: Closed layers must be traversed; calling layers cannot skip them. Open layers can be bypassed. Marking the business logic layer as closed forces all requests through business validation, even simple reads. Marking it open allows the presentation layer to bypass it for trivial operations. Most architectures use closed layers for write operations and consider opening layers for read operations.

Layer Granularity: Coarse-grained layers contain broad responsibilities. The business logic layer handles all domain concerns. Fine-grained layers split responsibilities further. The architecture might separate read models from write models, or domain services from application services. Finer granularity increases complexity but improves targeted changes.

Vertical Slicing vs Horizontal Layers: Pure layered architecture organizes code horizontally. All controllers live together, all services live together, all repositories live together. Vertical slicing organizes code by feature. The user registration feature includes its controller, service, and repository in one directory. Hybrid approaches use layers within feature slices.

Trade-offs to evaluate:

The architecture increases code volume. Each layer adds classes and interfaces. A simple operation touches multiple files. This overhead makes sense for complex domains but adds friction for simple applications.

The architecture can harm performance. Each layer crossing has cost. Function calls accumulate. Object serialization between layers adds overhead. Caching strategies must account for multiple layers. Microservices using layered architecture within each service compound this effect.

The architecture makes following a request path more difficult. Developers trace execution through multiple classes and indirection points. Debugging requires stepping through several layers. Understanding data transformation from HTTP request to database query requires examining multiple files.

The architecture fights against some framework designs. Ruby on Rails emphasizes convention over configuration and fast development. Rails encourages placing business logic in Active Record models, which collapses the business and data access layers. Forcing strict layering onto Rails requires fighting framework conventions.

Implementation Approaches

Traditional Three-Tier: Divides the application into presentation, business logic, and data access. Each tier runs on separate physical machines or containers. The web server handles presentation, the application server runs business logic, and the database server manages persistence. This approach maps directly to network topology and enables horizontal scaling of each tier independently.

Implementation involves creating clear module boundaries. The presentation tier imports business logic interfaces but never imports data access code. The business logic tier imports data access interfaces but never imports presentation code. Dependency injection connects the layers at runtime.

Four-Tier with Service Layer: Adds an application service layer between the presentation and domain layers. Controllers become thin adapters that delegate to application services. Application services orchestrate use cases by coordinating domain services and repositories. Domain services contain pure business logic with no infrastructure concerns.

This approach separates HTTP concerns from use case orchestration. The application service layer handles transaction boundaries, logging, and authorization checks. The domain layer focuses exclusively on business rules. Testing becomes simpler because domain logic has no web framework dependencies.

Domain-Driven Design Layers: Organizes layers around the domain model. The domain layer sits at the center with no dependencies on other layers. The application layer orchestrates domain objects to fulfill use cases. The infrastructure layer implements technical concerns like persistence and messaging. The presentation layer adapts between HTTP and application service interfaces.

This inverts the traditional dependency direction. The domain layer defines repository interfaces that the infrastructure layer implements. The application layer depends on abstractions that infrastructure provides. This allows the domain model to evolve independently of technical decisions.

# DDD-style layered architecture
module Domain
  class Order
    attr_reader :items, :status
    
    def initialize(items:)
      @items = items
      @status = :pending
    end
    
    def place
      raise "Order is empty" if items.empty?
      @status = :placed
    end
  end
  
  module Repositories
    class OrderRepository
      def save(order)
        raise NotImplementedError
      end
    end
  end
end

module Infrastructure
  class PostgresOrderRepository < Domain::Repositories::OrderRepository
    def save(order)
      OrderRecord.create(
        items: order.items,
        status: order.status
      )
    end
  end
end

module Application
  class PlaceOrderService
    def initialize(order_repository:)
      @order_repository = order_repository
    end
    
    def execute(items:)
      order = Domain::Order.new(items: items)
      order.place
      @order_repository.save(order)
    end
  end
end

module Presentation
  class OrdersController
    def create
      service = Application::PlaceOrderService.new(
        order_repository: Infrastructure::PostgresOrderRepository.new
      )
      service.execute(items: params[:items])
      render json: { status: :created }
    end
  end
end

Hexagonal Architecture Layers: Places the domain at the center with ports defining required and provided interfaces. Adapters implement ports to connect the domain to external systems. Inbound adapters (HTTP controllers, message handlers) invoke the domain. Outbound adapters (database repositories, API clients) satisfy domain dependencies.

This approach emphasizes testability. The domain has no framework dependencies. Tests replace real adapters with test doubles. The application runs with different adapter combinations for development, testing, and production environments.

Vertical Slice Architecture: Organizes layers within feature slices rather than across the application. Each feature contains its own presentation, business logic, and data access code. Features share infrastructure code but remain independent otherwise. This reduces coupling between features at the cost of potential duplication.

Each slice can choose appropriate patterns. Simple features use thin layers. Complex features use rich layers. Features evolve independently without coordinating changes across the entire application.

Common Patterns

Repository Pattern: Encapsulates data access behind a collection-like interface. Repositories expose methods that reflect domain operations (find_active_users, save_order) rather than database operations (SELECT, INSERT). The pattern isolates query construction and OR/M mapping from business logic.

class UserRepository
  def find_active_users
    User.where(status: 'active').order(:created_at)
  end
  
  def find_by_email(email)
    User.find_by(email: email)
  end
  
  def save(user)
    user.save!
  end
end

class UserService
  def initialize(repository: UserRepository.new)
    @repository = repository
  end
  
  def activate_account(email)
    user = @repository.find_by_email(email)
    user.activate
    @repository.save(user)
  end
end

Service Layer Pattern: Defines application boundaries and establishes transaction control. Service objects represent use cases or operations. Each service method implements a complete user action with defined inputs and outputs. Services orchestrate domain objects and coordinate repository calls.

Services handle cross-cutting concerns like authorization, logging, and error handling. They establish transaction boundaries to maintain consistency. Services provide stable interfaces that controllers and background jobs invoke.

class OrderFulfillmentService
  def initialize(
    order_repo: OrderRepository.new,
    inventory_repo: InventoryRepository.new,
    notification_service: NotificationService.new
  )
    @order_repo = order_repo
    @inventory_repo = inventory_repo
    @notification_service = notification_service
  end
  
  def fulfill_order(order_id)
    ActiveRecord::Base.transaction do
      order = @order_repo.find(order_id)
      
      order.items.each do |item|
        @inventory_repo.decrement_stock(item.product_id, item.quantity)
      end
      
      order.mark_as_fulfilled
      @order_repo.save(order)
      
      @notification_service.send_fulfillment_notice(order)
    end
  end
end

Data Transfer Object Pattern: Transfers data between layers using simple objects without behavior. DTOs decouple layer interfaces from internal representations. The presentation layer receives DTOs from the business layer and serializes them to JSON. The business layer receives DTOs from the presentation layer and constructs domain objects.

This pattern prevents exposing internal structure to external consumers. Domain objects contain business logic and might include circular references or lazy-loaded associations. DTOs contain only data needed for the current operation in a flat structure.

class UserDTO
  attr_accessor :id, :name, :email, :role
  
  def initialize(id:, name:, email:, role:)
    @id = id
    @name = name
    @email = email
    @role = role
  end
  
  def self.from_domain(user)
    new(
      id: user.id,
      name: user.full_name,
      email: user.email,
      role: user.role.to_s
    )
  end
end

class UserService
  def get_user_details(user_id)
    user = User.find(user_id)
    UserDTO.from_domain(user)
  end
end

class UsersController
  def show
    dto = UserService.new.get_user_details(params[:id])
    render json: dto
  end
end

Facade Pattern: Provides a simplified interface to complex subsystems within a layer. Facades hide internal complexity and reduce coupling between layers. A service facade might coordinate multiple domain services and repositories behind a single interface.

class OrderFacade
  def initialize
    @order_service = OrderService.new
    @payment_service = PaymentService.new
    @shipping_service = ShippingService.new
  end
  
  def place_order(order_params, payment_params, shipping_params)
    order = @order_service.create_order(order_params)
    payment = @payment_service.process_payment(payment_params)
    shipment = @shipping_service.schedule_shipment(shipping_params)
    
    {
      order: order,
      payment: payment,
      shipment: shipment
    }
  end
end

Anti-Pattern: Anemic Domain Model: Creates domain objects that contain only data with no behavior. All logic lives in service objects. This defeats the purpose of object-oriented design and reduces the business logic layer to a procedural script. Symptoms include models with only getters and setters, service objects with names like UserService that contain all user-related logic, and violation of the "Tell, Don't Ask" principle.

Anti-Pattern: Layer Leakage: Occurs when lower layers depend on upper layers or when layers skip intermediaries inappropriately. Controllers that directly call repositories bypass business logic validation. Domain models that reference HTTP requests or session state create upward dependencies. Data access objects that return ActiveRecord instances instead of domain objects couple consumers to OR/M choices.

Ruby Implementation

Ruby on Rails provides components that map to layered architecture but encourages patterns that diverge from strict layering. Rails emphasizes developer productivity and convention over explicit structure. Understanding both traditional layering and Rails conventions enables choosing appropriate patterns for each application.

Presentation Layer in Rails: Controllers handle HTTP concerns and delegate to service objects or directly to models. Views render HTML or JSON responses. Serializers convert domain objects to API responses. Routes map URLs to controller actions.

# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def create
    service = Orders::CreateService.new(current_user)
    result = service.call(order_params)
    
    if result.success?
      render json: OrderSerializer.new(result.order), status: :created
    else
      render json: { errors: result.errors }, status: :unprocessable_entity
    end
  end
  
  private
  
  def order_params
    params.require(:order).permit(:items, :shipping_address)
  end
end

# app/serializers/order_serializer.rb
class OrderSerializer
  def initialize(order)
    @order = order
  end
  
  def as_json
    {
      id: @order.id,
      status: @order.status,
      total: @order.total_amount,
      items: @order.items.map { |item| ItemSerializer.new(item).as_json }
    }
  end
end

Business Logic Layer in Rails: Service objects implement use cases. Interactors coordinate multiple operations. Domain models contain business rules and validations. Rails developers often create app/services/ or app/interactors/ directories for these classes.

# app/services/orders/create_service.rb
module Orders
  class CreateService
    def initialize(user)
      @user = user
      @order_repository = OrderRepository.new
      @inventory_service = InventoryService.new
    end
    
    def call(order_params)
      ActiveRecord::Base.transaction do
        validate_inventory(order_params[:items])
        
        order = build_order(order_params)
        reserve_inventory(order)
        
        @order_repository.save(order)
        
        Result.new(success: true, order: order)
      end
    rescue InventoryError => e
      Result.new(success: false, errors: [e.message])
    end
    
    private
    
    def validate_inventory(items)
      items.each do |item|
        unless @inventory_service.available?(item[:product_id], item[:quantity])
          raise InventoryError, "Insufficient stock for product #{item[:product_id]}"
        end
      end
    end
    
    def build_order(params)
      Order.new(
        user: @user,
        items: params[:items],
        shipping_address: params[:shipping_address],
        status: 'pending'
      )
    end
    
    def reserve_inventory(order)
      order.items.each do |item|
        @inventory_service.reserve(item.product_id, item.quantity)
      end
    end
  end
  
  class Result
    attr_reader :order, :errors
    
    def initialize(success:, order: nil, errors: [])
      @success = success
      @order = order
      @errors = errors
    end
    
    def success?
      @success
    end
  end
end

Data Access Layer in Rails: Active Record models handle persistence. Repository classes wrap Active Record to provide domain-oriented interfaces. Query objects encapsulate complex queries. Rails defaults to putting persistence logic in models, but explicit repositories improve testability and decouple business logic from OR/M details.

# app/repositories/order_repository.rb
class OrderRepository
  def find(id)
    order_record = OrderRecord.find(id)
    map_to_domain(order_record)
  end
  
  def find_pending_orders
    OrderRecord.where(status: 'pending').map { |record| map_to_domain(record) }
  end
  
  def save(order)
    record = OrderRecord.find_or_initialize_by(id: order.id)
    record.assign_attributes(
      user_id: order.user_id,
      status: order.status,
      total: order.total_amount
    )
    record.save!
    order.id = record.id
    order
  end
  
  private
  
  def map_to_domain(record)
    Order.new(
      id: record.id,
      user_id: record.user_id,
      status: record.status,
      items: record.order_items.map { |item| map_item_to_domain(item) }
    )
  end
  
  def map_item_to_domain(item_record)
    OrderItem.new(
      product_id: item_record.product_id,
      quantity: item_record.quantity,
      price: item_record.price
    )
  end
end

# app/queries/orders/pending_orders_query.rb
module Orders
  class PendingOrdersQuery
    def initialize(relation = OrderRecord.all)
      @relation = relation
    end
    
    def call
      @relation
        .where(status: 'pending')
        .where('created_at > ?', 7.days.ago)
        .includes(:user, :order_items)
        .order(created_at: :desc)
    end
  end
end

Organizing Layer Structure: Rails directory structure follows convention. Creating explicit layer directories makes architecture visible. Common approaches include:

# Feature-based organization
app/
  features/
    orders/
      controllers/
      services/
      repositories/
      models/
    users/
      controllers/
      services/
      repositories/
      models/

# Layer-based organization
app/
  controllers/
  services/
    orders/
    users/
  repositories/
    orders/
    users/
  models/

# Hybrid approach
app/
  controllers/
  models/
  services/        # Application and domain services
  repositories/    # Data access
  queries/         # Complex query objects
  serializers/     # Response formatting

Dependency Injection in Ruby: Ruby lacks built-in dependency injection containers. Manual injection through constructors provides explicit dependencies. The dry-container and dry-auto_inject gems provide dependency injection frameworks for Ruby applications.

# Manual dependency injection
class OrderService
  def initialize(repository: OrderRepository.new,
                 inventory_service: InventoryService.new,
                 notifier: EmailNotifier.new)
    @repository = repository
    @inventory_service = inventory_service
    @notifier = notifier
  end
end

# Using dry-container
class AppContainer
  extend Dry::Container::Mixin
  
  register(:order_repository) { OrderRepository.new }
  register(:inventory_service) { InventoryService.new }
  register(:email_notifier) { EmailNotifier.new }
  
  register(:order_service) do
    OrderService.new(
      repository: resolve(:order_repository),
      inventory_service: resolve(:inventory_service),
      notifier: resolve(:email_notifier)
    )
  end
end

Practical Examples

E-commerce Order Processing: An online store implements layered architecture to separate HTTP handling, business rules, and data persistence. The presentation layer handles product browsing and cart management. The business logic layer enforces pricing rules, inventory constraints, and order validation. The data access layer manages product catalogs, customer records, and order history.

# Presentation Layer
class CheckoutController < ApplicationController
  def create
    checkout_service = CheckoutService.new(current_user)
    result = checkout_service.process_checkout(
      cart_items: params[:items],
      shipping_address: params[:shipping_address],
      payment_method: params[:payment_method]
    )
    
    if result.success?
      render json: { order_id: result.order.id }, status: :created
    else
      render json: { errors: result.errors }, status: :unprocessable_entity
    end
  end
end

# Business Logic Layer
class CheckoutService
  def initialize(user)
    @user = user
    @pricing_service = PricingService.new
    @inventory_service = InventoryService.new
    @payment_service = PaymentService.new
    @order_repository = OrderRepository.new
  end
  
  def process_checkout(cart_items:, shipping_address:, payment_method:)
    validate_cart_items(cart_items)
    
    ActiveRecord::Base.transaction do
      order = build_order(cart_items, shipping_address)
      apply_pricing_rules(order)
      reserve_inventory(order)
      process_payment(order, payment_method)
      
      @order_repository.save(order)
      Result.new(success: true, order: order)
    end
  rescue CheckoutError => e
    Result.new(success: false, errors: [e.message])
  end
  
  private
  
  def validate_cart_items(items)
    raise CheckoutError, "Cart is empty" if items.empty?
    
    items.each do |item|
      product = ProductRepository.new.find(item[:product_id])
      raise CheckoutError, "Product not available" unless product.available?
    end
  end
  
  def build_order(items, address)
    Order.new(
      user: @user,
      items: items.map { |i| build_order_item(i) },
      shipping_address: address,
      status: 'pending'
    )
  end
  
  def build_order_item(item_data)
    product = ProductRepository.new.find(item_data[:product_id])
    OrderItem.new(
      product_id: product.id,
      quantity: item_data[:quantity],
      unit_price: product.price
    )
  end
  
  def apply_pricing_rules(order)
    order.subtotal = @pricing_service.calculate_subtotal(order.items)
    order.discount = @pricing_service.calculate_discount(order, @user)
    order.tax = @pricing_service.calculate_tax(order.subtotal - order.discount)
    order.total = order.subtotal - order.discount + order.tax
  end
  
  def reserve_inventory(order)
    order.items.each do |item|
      unless @inventory_service.reserve(item.product_id, item.quantity)
        raise CheckoutError, "Failed to reserve inventory"
      end
    end
  end
  
  def process_payment(order, payment_method)
    payment_result = @payment_service.charge(
      amount: order.total,
      method: payment_method,
      user: @user
    )
    
    unless payment_result.success?
      raise CheckoutError, "Payment failed: #{payment_result.error}"
    end
    
    order.payment_transaction_id = payment_result.transaction_id
  end
end

# Data Access Layer
class OrderRepository
  def save(order)
    record = OrderRecord.create!(
      user_id: order.user.id,
      status: order.status,
      subtotal: order.subtotal,
      discount: order.discount,
      tax: order.tax,
      total: order.total,
      shipping_address: order.shipping_address,
      payment_transaction_id: order.payment_transaction_id
    )
    
    order.items.each do |item|
      OrderItemRecord.create!(
        order_id: record.id,
        product_id: item.product_id,
        quantity: item.quantity,
        unit_price: item.unit_price
      )
    end
    
    order.id = record.id
    order
  end
  
  def find(id)
    record = OrderRecord.includes(:order_items).find(id)
    map_to_domain(record)
  end
  
  private
  
  def map_to_domain(record)
    Order.new(
      id: record.id,
      user: User.find(record.user_id),
      status: record.status,
      items: record.order_items.map { |i| map_item_to_domain(i) },
      subtotal: record.subtotal,
      discount: record.discount,
      tax: record.tax,
      total: record.total
    )
  end
end

Multi-Tenant SaaS Application: A project management application serves multiple organizations with isolated data. The presentation layer authenticates users and determines tenant context. The business logic layer enforces tenant isolation and permission rules. The data access layer scopes all queries to the current tenant.

# Presentation Layer - Tenant Context
class ApplicationController < ActionController::API
  before_action :authenticate_user!
  before_action :set_current_tenant
  
  private
  
  def set_current_tenant
    tenant = Tenant.find_by!(subdomain: request.subdomain)
    CurrentTenant.set(tenant)
  end
end

class ProjectsController < ApplicationController
  def index
    projects = ProjectService.new.list_projects(current_user)
    render json: projects
  end
  
  def create
    service = ProjectService.new
    result = service.create_project(
      owner: current_user,
      attributes: project_params
    )
    
    if result.success?
      render json: result.project, status: :created
    else
      render json: { errors: result.errors }, status: :unprocessable_entity
    end
  end
end

# Business Logic Layer - Tenant Isolation
class ProjectService
  def initialize
    @project_repository = ProjectRepository.new
    @permission_service = PermissionService.new
  end
  
  def list_projects(user)
    raise UnauthorizedError unless @permission_service.can_view_projects?(user)
    
    @project_repository.find_by_tenant(CurrentTenant.id)
  end
  
  def create_project(owner:, attributes:)
    raise UnauthorizedError unless @permission_service.can_create_project?(owner)
    
    project = Project.new(
      tenant_id: CurrentTenant.id,
      owner: owner,
      name: attributes[:name],
      description: attributes[:description]
    )
    
    @project_repository.save(project)
    Result.new(success: true, project: project)
  rescue ValidationError => e
    Result.new(success: false, errors: [e.message])
  end
end

# Data Access Layer - Scoped Queries
class ProjectRepository
  def find_by_tenant(tenant_id)
    ProjectRecord
      .where(tenant_id: tenant_id)
      .includes(:owner, :members)
      .map { |record| map_to_domain(record) }
  end
  
  def save(project)
    record = ProjectRecord.create!(
      tenant_id: project.tenant_id,
      owner_id: project.owner.id,
      name: project.name,
      description: project.description
    )
    
    project.id = record.id
    project
  end
end

# Thread-safe tenant context
class CurrentTenant
  def self.set(tenant)
    Thread.current[:current_tenant] = tenant
  end
  
  def self.id
    Thread.current[:current_tenant]&.id or raise "No tenant set"
  end
  
  def self.clear
    Thread.current[:current_tenant] = nil
  end
end

API Gateway with Multiple Backends: A mobile application backend aggregates data from multiple microservices. The presentation layer handles authentication and request routing. The business logic layer coordinates requests to backend services and combines results. The data access layer manages connections to different microservices and databases.

# Presentation Layer
class DashboardController < ApplicationController
  def show
    dashboard_service = DashboardService.new
    result = dashboard_service.get_user_dashboard(current_user)
    
    render json: DashboardSerializer.new(result).as_json
  end
end

# Business Logic Layer - Service Coordination
class DashboardService
  def initialize
    @user_service = UserService.new
    @activity_service = ActivityService.new
    @analytics_service = AnalyticsService.new
  end
  
  def get_user_dashboard(user)
    profile = @user_service.get_profile(user.id)
    recent_activity = @activity_service.get_recent_activity(user.id, limit: 10)
    stats = @analytics_service.get_user_stats(user.id)
    
    Dashboard.new(
      profile: profile,
      recent_activity: recent_activity,
      stats: stats
    )
  rescue ServiceError => e
    # Handle partial failures gracefully
    Dashboard.new(
      profile: profile,
      recent_activity: [],
      stats: nil,
      errors: [e.message]
    )
  end
end

# Data Access Layer - Service Clients
class ActivityService
  def initialize(http_client: HTTPClient.new)
    @client = http_client
    @base_url = ENV['ACTIVITY_SERVICE_URL']
  end
  
  def get_recent_activity(user_id, limit:)
    response = @client.get("#{@base_url}/users/#{user_id}/activity", {
      limit: limit
    })
    
    parse_activity_response(response)
  rescue HTTPError => e
    raise ServiceError, "Failed to fetch activity: #{e.message}"
  end
  
  private
  
  def parse_activity_response(response)
    JSON.parse(response.body)['activities'].map do |item|
      Activity.new(
        id: item['id'],
        type: item['type'],
        description: item['description'],
        timestamp: Time.parse(item['timestamp'])
      )
    end
  end
end

Reference

Layer Responsibilities

Layer Primary Responsibilities Prohibited Actions
Presentation HTTP handling, input validation, response formatting, session management Business logic, direct database access, domain rules
Business Logic Domain rules, workflow orchestration, transaction management, validation HTTP details, SQL queries, framework dependencies
Data Access Query construction, OR/M mapping, transaction handling, caching Business decisions, HTTP handling, view rendering
Database Data storage, query execution, transaction support, indexing Application logic, validation, formatting

Common Layer Patterns

Pattern Purpose Implementation
Repository Abstract data access Collection-like interface for domain objects
Service Layer Define application boundaries Use case classes with transaction control
Data Transfer Object Transfer data between layers Simple objects with no behavior
Facade Simplify subsystem interface Single entry point to complex subsystem
Unit of Work Track changes and coordinate writes Transaction scope with change tracking

Dependency Direction Rules

Rule Description Example
Downward Dependencies Upper layers depend on lower layers Controllers depend on services
No Upward Dependencies Lower layers never depend on upper layers Repositories do not reference controllers
Interface Segregation Depend on abstractions, not implementations Business logic depends on repository interface
Acyclic Dependencies No circular dependencies between layers Service A cannot depend on Service B if B depends on A

Ruby Layer Organization

Approach Directory Structure Use Case
Rails Default app/controllers, app/models, app/views Simple applications, rapid prototyping
Service Objects app/services/feature/action_service.rb Adding business logic layer to Rails
Feature Slices app/features/orders/controllers, services, repositories Large applications with many features
DDD Modules app/domain, app/application, app/infrastructure Complex domain models with rich behavior

Layer Interface Patterns

Pattern Input Output Error Handling
Command Pattern Command object with parameters Result object with success/failure Return result object with errors
Query Pattern Query parameters as hash Collection or single object Raise exception or return nil
Result Object Operation parameters Result with success flag and payload Errors in result object
Service Method Primitive types or DTOs Domain objects or DTOs Raise domain exceptions

Testing Strategy by Layer

Layer Test Type Focus Dependencies
Presentation Controller tests HTTP routing, response formatting, authentication Mock services, stub repositories
Business Logic Service tests Business rules, workflow logic, validation Mock repositories, use test doubles
Data Access Repository tests Query correctness, OR/M mapping Use test database, seed data
Integration End-to-end tests Complete flows across all layers Use test database and real services