Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Layered Design for Ruby on Rails Applications
Layered Design for Ruby on Rails Applications

Layered Design for Ruby on Rails Applications: Discover practical design patterns for maintainable web applications

eBook
€20.98 €29.99
Paperback
€37.99
Subscription
Free Trial
Renews at €18.99p/m

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Table of content icon View table of contents Preview book icon Preview Book

Layered Design for Ruby on Rails Applications

Rails as a Web Application Framework

Ruby on Rails is one of the most popular tools to build web applications, which is a huge class of software. In this chapter, we will talk about what makes this class different from other programs. First, we will learn about the HTTP request-response model and how it can naturally lead to a layered architecture. We will see which layers and HTTP components Ruby on Rails includes out of the box. Then, we will discuss the off-request processing layer, background jobs, and the persistence layer (databases).

In this chapter, we will cover the following topics:

  • The journey of a click through Rails abstraction layers
  • Beyond requests – background and scheduled tasks
  • The heart of a web application – the database

By the end of this chapter, you’ll have a better understanding of the core web application principles and how they affect Rails application design. You will learn about the main Rails components and how they build up the basic abstraction layers of the application.

These fundamental ideas will help you to identify and extract abstractions that better fit natural web application flows, thus leading to less conceptual overhead and a better developer experience.

Technical requirements

In this chapter and all chapters of this book, the code given in code blocks is designed to be executed on Ruby 3.2 and, where applicable, using Rails 7. Many of the code examples will work on earlier versions of the aforementioned software.

You will find the code files on GitHub at https://github.com/PacktPublishing/Layered-Design-for-Ruby-on-Rails-Applications/tree/main/Chapter01.

The journey of a click through Rails abstraction layers

The primary goal of any web application is to serve web requests, where web implies communicating over the internet and request refers to data that must be processed and acknowledged by a server.

A simple task such as clicking on a link and opening a web page in a browser, which we perform hundreds of times every day, consists of dozens of steps, from resolving the IP address of a target service to displaying the response to the user.

In the modern world, every request passes through multiple intermediate servers (proxies, load balancers, content delivery networks (CDNs), and so on). For this chapter, the following simplified diagram will be enough to visualize the journey of a click in the context of a Rails app.

Figure 1.1 – A simplified diagram of a journey of the click (the Rails version)

Figure 1.1 – A simplified diagram of a journey of the click (the Rails version)

The Rails part of this journey starts in a so-called web server – for example, Puma (https://github.com/puma/puma). It takes care of handling connections, transforming HTTP requests into a Ruby-friendly format, calling our Rails application, and sending the result back over the HTTP connection.

Communication models

Web applications can use other communication models, and not only the request-response one. Streaming and asynchronous (for example, WebSocket) models are not rare guests in modern Rails applications, especially after the addition of Hotwire (https://hotwired.dev/) to the stack. However, they usually play a secondary role, and most applications are still designed with the request-response model in mind. That’s why we only consider this model in this book.

Next, we will take a deeper look at the right part of the diagram in Figure 1.1. Getting to know the basics of request processing in Rails will help us to think in abstraction layers when designing our application. But first, we need to explain why layered architecture makes sense to web applications at all.

From web requests to abstraction layers

The life cycle of a web application consists of the bootstrap phase (configuration and initialization) and the serving phase. The bootstrap phase includes loading the application code, and initializing and configuring the framework components – that is, everything we need to do before accepting the first web request – before we enter the serving phase.

In the serving phase, the application acts as an executor, performing many independent units of work – handling web requests. Independent here means that every request is self-contained, and the way we process it (from a code point of view) doesn’t depend on previous or concurrent requests. This means that requests do not share a lot of state. In Ruby terms, when processing a request, we create many disposable objects, whose lifetimes are bound by the request’s lifetime.

How does this affect our application design? Since requests are independent, the serving phase could be seen as a conveyor-belt assembly line – we put request data (raw material) on the belt, pass it through multiple workstations, and get the response box at the end.

A natural reflection of this idea in application design would be the extraction of abstraction layers (workstations) and chaining them together to build a processing line. This process could also be called layering. Just like how assembly lines increase production efficiency in real life, architecture patterns improve software quality. In this book, we will discuss the layered architecture pattern, which is generic enough to fit many applications, especially Ruby on Rails ones.

What are the properties of a good abstraction layer? We will try to find the answer to this question throughout the book using examples; however, we can list some basic properties right away:

  • An abstraction should have a single responsibility. However, the responsibilities themselves can be broad but should not overlap (thus, following the separation of concerns principle).
  • Layers should be loosely coupled and have no circular or reverse dependencies. If we draw the request processing flow from top to bottom, the inter-layer connectors should never go up, and we should try to minimize the number of connections between layers. A physical assembly line is an example of perfect layering – every workstation (layer) has, at most, one workstation above and, at most, one below.
  • Abstractions should not leak their internals. The main idea of extracting an abstraction is to separate an interface from the implementation. Extracting a common interface can be a challenging task by itself, but it always pays off in the long term.
  • It should be possible to test abstractions in isolation. This item is usually a result of all the preceding, but it makes sense to pay attention to it explicitly, since thinking about testability can help us to come up with a better interface.

From a developer’s perspective, a good abstraction layer provides a clear interface to solve a common problem and is easy to refactor, debug, and test. A clear interface can be translated as one with the least possible conceptual overhead or just one that is simple.

Designing simple abstractions is a difficult task; that’s why you may hear that introducing abstractions makes working with the code base more complicated. The goal of this book is to teach you how to avoid this pitfall and learn how to design good abstractions.

How many abstraction layers are nice to have? The short answer is, it depends.

Let’s continue our assembly line analogy. The number of workstations grows as the assembly process becomes more sophisticated. We can also split existing stages into multiple new ones to make the process more efficient, and to assemble faster. Similarly, the number of abstraction layers increases with the evolution of a project’s business logic and the code base growth.

In real life, the efficiency metric is speed; in software development, it is also speed – the speed of shipping new features. This metric depends on many factors, many of which are not related to how we write our code. From the code perspective, the main factor is maintainability – how easy it is to add new features and introduce changes to the existing ones (including fixing bugs).

Applying software design patterns and extracting abstraction layers are the two main tools to keep maintainability high. Does it mean the more abstractions we have the more maintainable our code is?

Surely not. No one builds a car assembly line consisting of thousands of workstations by the number of individual nuts and screws, right? So, should we software engineers avoid introducing new abstractions just for the sake of introducing new abstractions? Of course not!

Overengineering is not a made-up problem; it does exist. Adding a new abstraction should be evaluated. We will learn some techniques when we start discussing particular abstraction layers later in this book. Now, let’s move on to Rails and see what the framework offers us out of the box in terms of abstraction layers.

A basic Rails application comes with just three abstractions – controllers, models, and views. (You are invited to decide whether they fit our definition of good or not by yourself.) Such a small number allows us to start building things faster and focus on a product, instead of spending time to please the framework (as it would be if had a dozen different layers). This is the Rails way.

In this book, we will learn how to extend the Rails way – how to gradually introduce new abstraction layers without losing the focus on product development. First, we need to learn more about the Rails way itself. Let’s take a look at some of the components that make up this approach with regard to web requests.

Rack

The component responsible for HTTP-to-Ruby (and vice versa) translation is called Rack (https://github.com/rack/rack). More precisely, it’s an interface describing two fundamental abstractions – request and response.

Rack is the contract between a web server (for example, Puma or Unicorn) and a Ruby application. It can be described using the following source:

request_env = { "HTTP_HOST" => "www.example.com", …}
response = application.call(request_env)
status, headers, body_iterator = *response

Let’s examine each line of the preceding code:

  • The first one defines an HTTP request represented as a Hash. This Hash is called the request environment and contains HTTP headers and Rack-specific fields (such as rack.input to access the request body). This API and naming convention came from the old days of CGI web servers, which passed request data via environment variables.

Common Gateway Interface

Common Gateway Interface (CGI) is the first attempt to standardize the communication interface between web servers and applications. A CGI-compliant program must read request headers from env variables and the request body from STDIN and write the response to STDOUT. A CGI web server runs a new instance of the program for every request – an unaffordable luxury for today’s Rails applications. The FastCGI (https://fastcgi-archives.github.io/) protocol was developed to resolve this situation.

  • The second line calls a Rack-compatible application, which is anything that responds to #call. That’s the only required method.
  • The final line describes the structure of the return value. It is an array, consisting of three elements – a status code (integer), HTTP response headers (Hash), and an enumerable body (that is, anything that responds to #each and yields string values). Why is body not just a string? Using enumerables allows us to implement streaming responses, which could help us reduce memory allocation.

The simplest possible Rack application is just a Lambda returning a predefined response tuple. You can run it using the rackup command like this (note that the rackup gem must be installed):

$ rackup -s webrick --builder 'run ->(env) { [200, {}, ["Hello, Rack!"]] }'
[2022-07-25 11:15:44] INFO  WEBrick 1.7.0
[2022-07-25 11:15:44] INFO  WEBrick::HTTPServer#start: pid=85016 port=9292

Try to open a browser at http://localhost:9292 – you will see "Hello, Rack!" on a blank screen.

Rails on Rack

Where is the Rack’s #call method in a Rails application? Look at the config.ru file at the root of your Rails project. It’s a Rack configuration file, which describes how to run a Rack-compatible application (.ru stands for rack-up). You will see something like this:

require_relative "config/environment"
run Rails.application

Rails.application is a singleton instance of the Rails application, its web entry-point.

Now that we know where the Rails part of the click journey begins, let’s try to learn more about it.

The best way to see the amount of work a Rails app does while performing a unit of work is to trace all Ruby method calls during a single request-response cycle. For that, we can use the trace_location gem.

What a gem – trace_location

The trace_location (https://github.com/yhirano55/trace_location) gem is a curious developer’s little helper. Its main purpose is to learn what’s happening behind the scenes of simple APIs provided by libraries and frameworks. You will be surprised how complex the internals of the things you take for granted (say, user.save in Active Record) can be.

Designing simple APIs that solve complex problems shows true mastery of software development. Under the hood, this gem uses Ruby’s TracePoint API (https://rubyapi.org/3.2/o/tracepoint) – a powerful runtime introspection tool.

The fastest way to emulate web request handling is to open a Rails console (rails c) and run the following snippet:

request =
  Rack::MockRequest.env_for('http://localhost:3000')
TraceLocation.trace(format: :log) do
  Rails.application.call(request)
end

Look at the generated log file. Even for a new Rails application, the output would contain thousands of lines – serving a GET request in Rails is not a trivial task.

So, the number of Ruby methods invoked during an HTTP request is huge. What about the number of created Ruby objects? We can measure it using the built-in Ruby tools. In a Rails console, type the following:

was_alloc = GC.stat[:total_allocated_objects]
Rails.application.call(request)
new_alloc = GC.stat[:total_allocated_objects]
puts "Total allocations: #{new_alloc – was_alloc}"

For an action rendering nothing (head :ok), I get about 3,000 objects when running the preceding snippet. We can think of this number as a lower bound for Rails applications.

What do these numbers mean for us? The goal of this book is to demonstrate how we can leverage abstraction layers to keep our code base in a healthy state. At the same time, we shouldn’t forget about potential performance implications. Adding an abstraction layer results in adding more method calls and object allocations, but this overhead is negligible compared to what we already have. In Rails, abstractions do not make code slower (humans do).

Let’s run our tracer again and only include #call methods this time:

TraceLocation.trace(format: :log, methods: [:call]) do
  Rails.application.call(request)
end

This time, we only have a few hundred lines logged:

[Tracing events] C: Call, R: Return
C /usr/local/lib/ruby/gems/3.1.0/gems/railties-7.0.3.1/lib/rails/engine.rb:528 [Rails::Engine#call]
  C /usr/local/lib/ruby/gems/3.1.0/gems/actionpack-7.0.3.1/lib/action_dispatch/middleware/host_authorization.rb:130 [ActionDispatch::HostAuthorization#call]
    C /usr/local/lib/ruby/gems/3.1.0/gems/rack-2.2.4/lib/rack/sendfile.rb:109 [Rack::Sendfile#call]
      C /usr/local/lib/ruby/gems/3.1.0/gems/actionpack-7.0.3.1/lib/action_dispatch/middleware/static.rb:22 [ActionDispatch::Static#call]
         // more lines here
      R /usr/local/lib/ruby/gems/3.1.0/gems/actionpack-7.0.3.1/lib/action_dispatch/middleware/static.rb:24 [ActionDispatch::Static#call]
    R /usr/local/lib/ruby/gems/3.1.0/gems/rack-2.2.4/lib/rack/sendfile.rb:140 [Rack::Sendfile#call]
  R /usr/local/lib/ruby/gems/3.1.0/gems/actionpack-7.0.3.1/lib/action_dispatch/middleware/host_authorization.rb:131 [ActionDispatch::HostAuthorization#call]
R /usr/local/lib/ruby/gems/3.1.0/gems/railties-7.0.3.1/lib/rails/engine.rb:531 [Rails::Engine#call]

Each method is put twice in the log – first, when we enter it, and the second time when we return from it. Note that the #call methods are nested into each other; this is another important feature of Rack in action — middleware.

Pattern – middleware

Middleware is a component that wraps a core unit (function) execution and can inspect and modify input and output data without changing its interface. Middleware is usually chained, so each one invokes the next one, and only the last one in the chain executes the core logic. The chaining aims to keep middleware small and single-purpose. A typical use case for middleware is adding logging, instrumentation, or authentication (which short-circuits the chain execution). The pattern is popular in the Ruby community, and aside from Rack, it is used by Sidekiq, Faraday, AnyCable, and so on. In the non-Ruby world, the most popular example would be Express.js.

The following diagram shows how a middleware stack wraps the core functionality by intercepting inputs and enhancing outputs:

Figure 1.2 – The middleware pattern diagram

Figure 1.2 – The middleware pattern diagram

Rack is a modular framework, which allows you to extend basic request-handling functionality by injecting middleware. Middleware intercepts HTTP requests to perform some additional, usually utilitarian, logic – enhancing a Rack env object, adding additional response headers (for example, X-Runtime or CORS-related), logging the request execution, performing security checks, and so on.

Rails includes more than 20 middlewares by default. You can see the middleware stack by running the bin/rails middleware command:

$ bin/rails middleware
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActionDispatch::ServerTiming
use Rack::Runtime
... more ...
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run MyProject::Application.routes

Rails gives you full control of the middleware chain – you can add, remove, or rearrange middleware. The middleware stack can be called the HTTP pre-/post-processing layer. It should treat the application as a black box and know nothing about its business logic. A Rack middleware stack should not enhance the application web interface but act as a mediator between the outer world and the Rails application.

Rails routing

At the end of the preceding middleware list, there is a run command with Application.routes passed. The routes object (an instance of the ActionDispatch::Routing::RouteSet class) is a Rack application that uses the routes.rb file to match the request to a particular resolver – a controller-action pair or yet another Rack application.

Here is an example of the routing config with both controllers and applications:

Rails.application.routes.draw do
  # Define a resource backed by PostsController
  resources :posts, only: %i[show]
  # redirect() generates a Redirect Rack app instance
  get "docs/:article",
      to: redirect("/wiki/%{article}")
  # You can pass a lambda, too
  get "/_health", to: -> _env {
    [200, { "content-type" => "text/html" }, ["I'm alive"]]
  }
  # Proxy all matching requests to a Rack app
  mount ActionCable.server, at: "/cable"
end

This is the routing layer of a Rails application. All the preceding Rack resolvers can be implemented as Rack middleware; why did we put them into the routing layer? Redirects are a part of the application functionality, as well as WebSockets (Action Cable).

However, the health check endpoint can be seen as a property of a Rack app, and if it doesn’t use the application’s internal state to generate a response (as in our example), it can be moved to the middleware layer.

Similar to choosing between Rack and routes, we can have a routes versus controllers debate. With regards to the preceding example, we can ask, why not use controllers for redirects?

C for controller

Controllers comprise the next layer through which a web request passes. This is the first abstraction layer on our way. A controller is a concept that generalizes and standardizes the way we process inbound requests. In theory, controllers can be used not only for HTTP requests but also for any kind of request (since the controller is just a code abstraction).

In practice, however, that’s very unlikely – implementation-wise, controllers are highly coupled with HTTP/Rack. There is even an API to turn a controller’s action into a Rack app:

Rails.application.routes.draw do
  # The same as: resources :posts, only: %i[index]
  get "/posts", to: PostsController.action(:index)
end

MVC

Model–view–controller is one of the oldest architectural patterns, which was developed in the 1970s for GUI applications development. The pattern implies that a system consists of three components – Model, View, and Controller. Controller handles user actions and operates on Model; Model, in turn, updates View, which results in a UI update for the user. Although Rails is usually called an MVC framework, its data flow differs from the original one – Controller is responsible for updating View, and View can easily access and even modify Model.

The controller layer’s responsibility is to translate web requests into business actions or operations and trigger UI updates. This is an example of single responsibility, which consists of many smaller responsibilities – an actor (a user or an API client) authentication and authorization, requesting parameters validation, and so on. The same is true for every inbound abstraction layer, such as Action Cable channels or Action Mailbox mailboxes.

Coming back to the routing example and the redirects question, we can now answer it – since there is no business action behind the redirection logic, putting it into a controller is an abstraction misuse.

We will talk about controllers in detail in the following chapters.

Now, we have an idea of a click’s journey through a Rails application. However, not everything in Rails happens within a request-response cycle; our click has likely triggered some actions to be executed in the background.

Beyond requests – background and scheduled tasks

Although the primary goal of web applications is to handle HTTP requests, that’s not the only job most Rails applications do. A lot of things happen in the background.

The need for background jobs

One of the vital characteristics of web applications is throughput. This can be defined as the number of requests that can be served in a period of time – usually, a second or a minute (requests per second (RPS) or requests per minute (RPM), respectively).

Ruby web applications usually have a limited number of web workers to serve requests, and each worker is only capable of processing one request at a time. A worker is backed by a Ruby thread or a system process. Due to the Global Virtual Machine Lock (GVL), adding more threads doesn’t help us to increase the throughput. Usually, the number of threads is as low as three to five.

Choosing the right number of threads

Since Ruby 3.2, it’s been possible to measure the exact time a Ruby thread spends waiting for I/O using, for example, the GVLTools library (https://github.com/Shopify/gvltools). Knowing the exact time, you can choose the number of threads that fits your application the best.

Scaling with processes requires a proportional amount of RAM. We need to look for other solutions.

Beyond processes and threads – fibers

Ruby has the solution to this concurrency problem – fibers (https://rubyapi.org/3.2/o/fiber). We can describe it as a lighter alternative to a thread, which can be used for cooperative concurrency – a concurrency model in which the context switch is controlled by the user, not the VM. Since Ruby 3, fibers can automatically yield execution on I/O operations (networking, filesystem access, and so on), which makes them fit the web application use case well. Unfortunately, Rails itself is not fiber-ready yet, so we cannot fully use web servers that leverage this technology, such as Falcon (https://github.com/socketry/falcon).

For many years, Rails applications relied on the following idea – to minimize a request time (and, thus, increase throughput), we should offload as much work as possible to background execution. Libraries, such as Sidekiq and Delayed Job, brought this idea to life and popularized it, and later, with the release of Rails 4, Active Job made this approach official.

What a gem – sidekiq

Sidekiq (https://github.com/mperham/sidekiq) is one of the most popular Ruby gems and the number one background processing engine. It relies on the idea that background tasks are usually I/O heavy, and thus, we can efficiently scale processing by using Ruby threads. Sidekiq uses Redis as a queueing backend for jobs, which reduces the infrastructure overhead and positively impacts performance.

What is a background job? It is a task that’s executed outside of the request-response loop.

A typical example of such a task would be sending an email. To send an email, we must perform a network request (SMTP or HTTP), but we don’t need to wait for it to be completed to send a response back to the user. How can we break out of the synchronous request-handling operation in Ruby? We could use threads, which might look like this:

class PasswordResetController < ApplicationController
  def create
    user = User.find_by!(email: params[:email])
    Thread.new do
      UserMailer.with(user:).reset_password.deliver_now
    end
  end
end

This simple solution has a lot of rough edges – we do not control the number of threads, we do not handle potential exceptions, and we have no strategy on what to do if there are failures. Therefore, background job abstraction (Active Job, in particular) arose – to provide a general solution to common problems with asynchronous tasks.

Next, let’s talk about the fundamental concepts of background processing in Rails.

Jobs as units of work

The background job layer is built on top of job and queue abstractions.

Job is a task definition that includes the actual business logic and describes the processing-related properties, such as retrying logic and execution priority. The latter justifies the need for a separate abstraction, Job; pure Ruby objects are not enough.

Queues are natural for background jobs in web applications, since we usually want our offloaded tasks to be executed on a first in, first out basis. Background processing engines can use any data structure and/or storage mechanism to keep and execute tasks; we only need them to comply with the queue interface.

Figure 1.3 – A high-level overview of the background tasks queue architecture

Figure 1.3 – A high-level overview of the background tasks queue architecture

Background jobs are meant to be independent and can be executed concurrently (although we can enqueue jobs within jobs forming background workflows). Thus, similar to web requests, background jobs are also units of work.

Each unit of work in a Rails application can be associated with an execution context. Execution context is an environment (state) associated with a particular execution frame, which has a clearly defined beginning and end. For web requests, an environment is defined by an HTTP request and its properties (for example, user session). Background jobs do not have such natural environments but can define one. Thus, another utilitarian responsibility of a background job is to define an execution context for the corresponding business operation.

Thus, the background jobs layer can be seen as the internal inbound layer. Unlike external inbound layers (for example, controllers), we do not deal with user input here, and hence, no authentication, authorization, or validation is required. Otherwise, from a software design point of view, jobs can be treated the same way as controllers.

Most background jobs are initiated within web requests or other jobs. However, there is another potential trigger – time.

Scheduled jobs

Scheduled jobs are a special class of background jobs that are triggered by a clock, not as a result of a user action or another job execution. Besides that, there is no difference between scheduled jobs and regular background jobs.

However, since Rails doesn’t provide a solution to define a jobs schedule out of the box, it’s easy to escape from the abstraction and come up with a unique (that is, more difficult to maintain) solution.

For example, many gems, such as whenever (https://github.com/javan/whenever) or rufus-scheduler (https://github.com/jmettraux/rufus-scheduler), allow you to run arbitrary Ruby code or system commands on schedule, not only enqueuing background jobs.

Such custom jobs lack all the benefits of being executed by a background jobs engine – failure handling, logging, instrumentation, and so on. They also introduce additional conceptual complexity. Scheduled jobs belong to the same abstraction layer as regular background jobs and, thus, should be a part of the layer, not its own abstraction (or a lack of it).

We have covered the basics of Rails inbound layers, those responsible for triggering units of work essential for all web applications. Irrespective of the kind of request that initiates the work, in most situations such work in a web application would be associated with data reading and writing.

The heart of a web application – the database

A typical web application can be seen as an interface for data. Whether it’s a blogging platform, an e-commerce service, or a project-management tool, most user interactions are coupled with reading or storing some information. Of course, there are data-less web applications – for example, proxy services – but you’re unlikely to choose Ruby on Rails to implement them.

Data is likely to be the most valuable part of your product or service. Just imagine you accidentally dropped your production database and all the backups – could you carry on? The database is also usually the most heavily loaded component of your application. The overall performance of your application depends on how you use the database and keep it in a healthy state.

Thus, while designing our application, we should keep in mind possible performance degradations related to the database.

The trade-off between abstractions and database performance

One of the main purposes of abstractions is to hide away the implementation details. In theory, a user should not know what’s happening under the hood of a certain API method. Consider the following example:

class User
  def self.create(name:)
    DB.exec "INSERT INTO users (name) values (%)", name
  end
end
names = %w[lacey josh]
names.each { User.create(name: _1) }

The User class is our abstraction to work with a database. We added a convenient interface to insert new records into a database table, which is assumed to be used throughout the application.

However, this abstraction could be over-used, thus introducing additional load to our database – whenever we want to create N users, we execute N queries. If we didn’t use the abstraction, we would write a single "INSERT INTO…" statement – a much more performant way of achieving the same result.

This is just a basic example that demonstrates the following – hiding implementation details is not equal to not taking implementation specifics and limitations into account. Abstractions and APIs should be designed so as to make it harder to shoot yourself in the foot when using them.

One common technique, which leads to non-optimal database interactions, is using domain-specific languages (DSLs) to define query-building rules. DSLs are powerful tools, but with great power comes great responsibility.

Let’s look at a more realistic example using the CanCanCan (https://github.com/CanCanCommunity/cancancan) library. This library allows you to define authorization rules (abilities) using fancy DSL. The DSL defines a ruleset, which could be used to scope database records. Here is an example:

can :read, Article do |article|
  article.published_at <= Time.now
end

The rule states that only the already published articles can be accessed by users. This rule is used every time we call Article.all.accessible_by(user) (for example, when we want to show a user a list of articles on a home page). How do you think the scoping would be handled in this case?

If we wrote #accessible_by by hand, we would probably perform a single query to return the desired records – "SELECT * FROM articles WHERE published_at < now()". What will our library do? It will fetch and then filter all the records using the rule block.

The result is the same, but it would require much more system resources (memory to load a lot of records and additional CPU cycles to run the block many times). Luckily, CanCanCan allows you to add a hint on how to transform the block into a query condition:

can :read, Article, "published_at < now()" do |article|
  article.published_at <= Time.now
end

This is an example of a leaky abstraction, an abstraction that exposes implementation details to its users. In the preceding snippet, our DSL-based configuration file contains the parts of the underlying database query. In this case, this is a necessary evil. And it can also be seen as an indicator that we chose the wrong level of abstraction to solve the problem, and now we have to patch it.

When designing abstractions, we should think of potential performance implications beforehand to avoid leaky abstractions in the future.

Database-level abstractions

Abstractions need not be defined in the application code only; we can also benefit from using abstractions in the database.

The main motivation for considering this approach could be the application performance. Another possible reason is consistency – the database is the primary source of truth, and databases (relational) are usually good at enforcing consistency; thus, moving some logic to the database layer can minimize the risk of data becoming inconsistent.

Even though you can move all your business logic into a database by defining custom functions and procedures, that’s not the way web (and especially Rails) applications are built. It could be an ideal way if the only thing we cared about was performance, but we chose web frameworks for productivity.

Nevertheless, some functionality can be implemented at the database level and bring us performance and productivity benefits. Let’s consider particular examples.

One common task that can be handled at the database layer is keeping track of record changes (for audit purposes). We can implement this in our Ruby code by adding hooks everywhere we create, update, or delete records, or go the Rails way and define model-level callbacks (as PaperTrail (https://github.com/paper-trail-gem/paper_trail) does).

Alternatively, we can leverage database features, such as triggers, and make our database responsible for creating audit records (as Logidze does). The latter approach has the benefits of being more performant and reducing the code base complexity. It is worth considering when audit records are not first-class citizens of your business logic (that is, not involved in other processes beyond auditing).

What a gem – logidze

Logidze (https://github.com/palkan/logidze) is a combination of a database extension (via PostgreSQL functions) and a Ruby API to track individual record changes incrementally. It can be used as a general auditing tool and a time-travel machine (to quickly access older versions of a record).

Another potential use case for giving the database a bit more responsibility is soft deletion. Soft deletion is an approach where a record is marked as deleted (and made invisible for users) instead of removing it from the database whenever a logical delete operation should occur. This technique can be used to provide undo/restore functionality or for auditing purposes.

Besides performance considerations, we may want to add database abstractions for the sake of consistency. For example, in PostgreSQL, we can create domain types and composite types. Unlike general constraints, custom types are reusable and carry additional semantics. You can use the pg_trunk (https://github.com/nepalez/pg_trunk) gem to manage custom types from Rails (as well as other PostgreSQL-specific features).

In general, enhancing database logic with custom abstractions is viable if the purpose of the abstraction is to act as data middleware – that is, treat data in isolation from the application business logic. Technically, such isolation means that abstraction should be set up once and never changed. I use the term middleware here to underline the conceptual similarity with Rack middlewares.

Summary

In this chapter, you learned about the primary features and components of web applications. You learned about the abstraction layers present in Rails applications and how they correspond to the web nature of Rails and its MVC philosophy. You learned about the unit of work and execution context concepts and their relationship with the inbound abstraction layers.

You also learned about the potential trade-offs between abstractions and application performance and, in particular, the database. This chapter demonstrated the fundamental ideas behind the layered software architecture, which we will refer to a lot throughout the book.

In the next chapter, you’ll dig deeper into the M part of the MVC architecture and learn about the design ideas that make Active Record the most significant part of the Ruby on Rails framework.

Questions

  1. Do Rails core abstractions (controllers, models, and views) satisfy our requirements for good abstraction layers?
  2. How many abstraction layers should a Rails application have?
  3. Does the number of abstraction layers affect a Rails application’s performance?
  4. Which problem do we solve by moving execution to background jobs?
  5. What is a leaky abstraction?

Exercises

We learned that handling a web request involves thousands of method calls and allocated Ruby objects. What if skip the Rack middleware and pass the request to the router directly (Rails.application.routes.call(request))? What about skipping the router and calling a controller action right away (for example, PostsController.action(:index).call(request))? Using trace_location and GC.stats, conduct some experiments and analyze the results. What are the overheads of the Rack middleware and the router?

Further reading

Polished Ruby Programming (Section 3, Ruby Web Programming Principles): https://www.packtpub.com/product/polished-ruby-programming/9781801072724

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Understand Rails' architectural patterns along with its advantages and disadvantages
  • Organize business logic in Rails apps when the default approach is insufficient
  • Introduce new abstractions to address design problems
  • Purchase of the print or Kindle book includes a free PDF eBook

Description

The Ruby on Rails framework boosts productivity by leveraging the convention-over-configuration principle and model-view-controller (MVC) pattern, enabling developers to build features efficiently. This initial simplicity often leads to complexity, making a well-structured codebase difficult to maintain. Written by a seasoned software engineer and award-winning contributor to many other open-source projects, including Ruby on Rails and Ruby, this book will help you keep your code maintainable while working on a Rails app. You’ll get to grips with the framework’s capabilities and principles to harness the full potential of Rails, and tackle many common design problems by discovering useful patterns and abstraction layers. By implementing abstraction and dividing the application into manageable modules, you’ll be able to concentrate on specific parts of the app development without getting overwhelmed by the entire codebase. This also encourages code reuse, simplifying the process of adding new features and enhancing the application's capabilities. Additionally, you’ll explore further steps in scaling Rails codebase, such as service extractions. By the end of this book, you’ll become a code design specialist with a deep understanding of the Rails framework principles.

Who is this book for?

This book is for Rails application developers looking to efficiently manage the growing complexity of their projects. Whether you've recently launched your first Rails minimum viable product or are struggling to progress with a sizable monolithic application, this book is here to help. A deep understanding of core Rails principles is a must. Prior experience in building web apps using the Rails framework will help you understand and apply the concepts in the book in a better way.

What you will learn

  • Get to grips with Rails' core components and its request/response cycle
  • See how Rails' convention-over-configuration principle affects development
  • Explore patterns for software flexibility, extensibility, and testability in Rails
  • Identify and address Rails' anti-patterns for cleaner code
  • Implement design patterns for handling bloated models and messy views
  • Expand from mailers to multi-channel notification deliveries
  • Introduce different authorization models and layers to your codebase
  • Take a class-based approach to configuration in Rails
Estimated delivery fee Deliver to Finland

Premium delivery 7 - 10 business days

€17.95
(Includes tracking information)

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Aug 30, 2023
Length: 298 pages
Edition : 1st
Language : English
ISBN-13 : 9781801813785
Category :
Languages :

What do you get with Print?

Product feature icon Instant access to your digital eBook copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Estimated delivery fee Deliver to Finland

Premium delivery 7 - 10 business days

€17.95
(Includes tracking information)

Product Details

Publication date : Aug 30, 2023
Length: 298 pages
Edition : 1st
Language : English
ISBN-13 : 9781801813785
Category :
Languages :

Packt Subscriptions

See our plans and pricing
Modal Close icon
€18.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
€189.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts
€264.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total 113.97
React 18 Design Patterns and Best Practices
€33.99
Layered Design for Ruby on Rails Applications
€37.99
Polished Ruby Programming
€41.99
Total 113.97 Stars icon

Table of Contents

19 Chapters
Part 1: Exploring Rails and Its Abstractions Chevron down icon Chevron up icon
Chapter 1: Rails as a Web Application Framework Chevron down icon Chevron up icon
Chapter 2: Active Models and Records Chevron down icon Chevron up icon
Chapter 3: More Adapters, Less Implementations Chevron down icon Chevron up icon
Chapter 4: Rails Anti-Patterns? Chevron down icon Chevron up icon
Chapter 5: When Rails Abstractions Are Not Enough Chevron down icon Chevron up icon
Part 2: Extracting Layers from Models Chevron down icon Chevron up icon
Chapter 6: Data Layer Abstractions Chevron down icon Chevron up icon
Chapter 7: Handling User Input outside of Models Chevron down icon Chevron up icon
Chapter 8: Pulling Out the Representation Layer Chevron down icon Chevron up icon
Part 3: Essential Layers for Rails Applications Chevron down icon Chevron up icon
Chapter 9: Authorization Models and Layers Chevron down icon Chevron up icon
Chapter 10: Crafting the Notifications Layer Chevron down icon Chevron up icon
Chapter 11: Better Abstractions for HTML Views Chevron down icon Chevron up icon
Chapter 12: Configuration as a First-Class Application Citizen Chevron down icon Chevron up icon
Chapter 13: Cross-Layers and Off-Layers Chevron down icon Chevron up icon
Index Chevron down icon Chevron up icon
Gems and Patterns Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Top Reviews
Rating distribution
Full star icon Full star icon Full star icon Full star icon Half star icon 4.7
(16 Ratings)
5 star 87.5%
4 star 6.3%
3 star 0%
2 star 0%
1 star 6.3%
Filter icon Filter
Top Reviews

Filter reviews by




Simon Bennett Jul 29, 2024
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Feefo Verified review Feefo
Mike Goggin Sep 12, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
This book is mostly geared towards intermediate to senior-level Rails engineers, but more junior-level folks can still benefit from the information contained therein.The first several chapters are an overview of how Rails works which are essential for any engineer who wants to get the most out of Rails. The rest of the book are a collection of different design patterns that someone could employ to make an existing application easy to reason about.I wouldn't recommend starting with these advanced design patterns or else you run the risk of over-engineering and introducing too much complexity unnecessarily, but if you've got an existing app that's starting to look a little crufty, these patterns can help you to abstract away the cruft in more convention-y way.The content of the book is great. It's easy to read and follow. A bit dry at times (but what technical resource isn't dry?).
Amazon Verified review Amazon
Jeremy Smith Aug 31, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
I loved this book, and found it very timely given the many questions and opinions I've seen in the community over the past couple years, about how to grow Rails applications beyond what the framework gives you out-of-the-box.As an overarching concept, Vladimir lays out a compelling vision for an "Extended Rails Way" that first guides us on how to use the baked-in abstractions well, and then helps us imagine how we could extract new abstraction layers that really feel like they could be Rails.The book helped me see how I could turn design patterns into new, cohesive abstractions that rhyme with Rails, creating more harmonious codebases with reduced cognitive burden.Vladimir provides clear perspective and helpful recommendations around some of Rails' most contentious features, including callbacks and concerns.Throughout the book, there's a repeated pattern of showing less-than-ideal examples, and then reasoning our way into one or more improved solutions. I found observing his thinking here to be really informative and helpful.I was impressed by how comprehensive the book is, covering all the topics you'd expect, like Active Record and Active Storage, plus those you might not expect, like filter objects, notifications, view components, and even configuration and logging.Long story short, this is now one of two Rails books I'll be recommending that all Rails devs read.
Amazon Verified review Amazon
Niharika Oct 11, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
This Ruby on Rails ebook is a fantastic resource for mid-level developers with a foundational understanding of Ruby and Rails. It delves deep into essential topics like anti-patterns, the Rails framework, abstraction, and authorization. The explanations are clear and practical, making it easy to grasp complex concepts. With real-world examples and hands-on guidance, this book empowers developers to take their skills to the next level, helping them build more efficient and secure applications. If you're looking to strengthen your expertise in Ruby on Rails, this ebook is a valuable companion on your journey toward becoming a more proficient developer.
Amazon Verified review Amazon
Brittany J. Martin Aug 30, 2023
Full star icon Full star icon Full star icon Full star icon Full star icon 5
I've been a huge fan of Vladimir's content from his conference talks and from his work at Evil Martians. "Layered Design for Ruby on Rails Applications" is a game-changer for Rails developers. With clear explanations and practical examples, it takes you from the basics of layered architecture to advanced implementation. The hands-on approach, code snippets, and best practices make it incredibly valuable. Not just theory, it tackles real-world challenges and provides insights into refactoring. This book not only teaches layered design but also transforms how you approach clean and maintainable code. For anyone serious about enhancing their Ruby on Rails skills, this book is an indispensable resource.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

What is the delivery time and cost of print book? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela
What is custom duty/charge? Chevron down icon Chevron up icon

Customs duty are charges levied on goods when they cross international borders. It is a tax that is imposed on imported goods. These duties are charged by special authorities and bodies created by local governments and are meant to protect local industries, economies, and businesses.

Do I have to pay customs charges for the print book order? Chevron down icon Chevron up icon

The orders shipped to the countries that are listed under EU27 will not bear custom charges. They are paid by Packt as part of the order.

List of EU27 countries: www.gov.uk/eu-eea:

A custom duty or localized taxes may be applicable on the shipment and would be charged by the recipient country outside of the EU27 which should be paid by the customer and these duties are not included in the shipping charges been charged on the order.

How do I know my custom duty charges? Chevron down icon Chevron up icon

The amount of duty payable varies greatly depending on the imported goods, the country of origin and several other factors like the total invoice amount or dimensions like weight, and other such criteria applicable in your country.

For example:

  • If you live in Mexico, and the declared value of your ordered items is over $ 50, for you to receive a package, you will have to pay additional import tax of 19% which will be $ 9.50 to the courier service.
  • Whereas if you live in Turkey, and the declared value of your ordered items is over € 22, for you to receive a package, you will have to pay additional import tax of 18% which will be € 3.96 to the courier service.
How can I cancel my order? Chevron down icon Chevron up icon

Cancellation Policy for Published Printed Books:

You can cancel any order within 1 hour of placing the order. Simply contact customercare@packt.com with your order details or payment transaction id. If your order has already started the shipment process, we will do our best to stop it. However, if it is already on the way to you then when you receive it, you can contact us at customercare@packt.com using the returns and refund process.

Please understand that Packt Publishing cannot provide refunds or cancel any order except for the cases described in our Return Policy (i.e. Packt Publishing agrees to replace your printed book because it arrives damaged or material defect in book), Packt Publishing will not accept returns.

What is your returns and refunds policy? Chevron down icon Chevron up icon

Return Policy:

We want you to be happy with your purchase from Packtpub.com. We will not hassle you with returning print books to us. If the print book you receive from us is incorrect, damaged, doesn't work or is unacceptably late, please contact Customer Relations Team on customercare@packt.com with the order number and issue details as explained below:

  1. If you ordered (eBook, Video or Print Book) incorrectly or accidentally, please contact Customer Relations Team on customercare@packt.com within one hour of placing the order and we will replace/refund you the item cost.
  2. Sadly, if your eBook or Video file is faulty or a fault occurs during the eBook or Video being made available to you, i.e. during download then you should contact Customer Relations Team within 14 days of purchase on customercare@packt.com who will be able to resolve this issue for you.
  3. You will have a choice of replacement or refund of the problem items.(damaged, defective or incorrect)
  4. Once Customer Care Team confirms that you will be refunded, you should receive the refund within 10 to 12 working days.
  5. If you are only requesting a refund of one book from a multiple order, then we will refund you the appropriate single item.
  6. Where the items were shipped under a free shipping offer, there will be no shipping costs to refund.

On the off chance your printed book arrives damaged, with book material defect, contact our Customer Relation Team on customercare@packt.com within 14 days of receipt of the book with appropriate evidence of damage and we will work with you to secure a replacement copy, if necessary. Please note that each printed book you order from us is individually made by Packt's professional book-printing partner which is on a print-on-demand basis.

What tax is charged? Chevron down icon Chevron up icon

Currently, no tax is charged on the purchase of any print book (subject to change based on the laws and regulations). A localized VAT fee is charged only to our European and UK customers on eBooks, Video and subscriptions that they buy. GST is charged to Indian customers for eBooks and video purchases.

What payment methods can I use? Chevron down icon Chevron up icon

You can pay with the following card types:

  1. Visa Debit
  2. Visa Credit
  3. MasterCard
  4. PayPal
What is the delivery time and cost of print books? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela