doc/development/backend/ruby_style_guide.md
This is a GitLab-specific style guide for Ruby code. Everything documented in this page can be reopened for discussion.
We use RuboCop to enforce Ruby style guide rules.
Where a RuboCop rule is absent, refer to the following style guides as general guidelines to write idiomatic Ruby:
Generally, if a style is not covered by existing RuboCop rules or the above style guides, it shouldn't be a blocker.
Some styles we have decided no one should not have a strong opinion on.
See also:
These styles are not backed by a RuboCop rule.
For every style added to this section, link the discussion from the section's history note to provide context and serve as a reference.
attr_readerInstance variables can be accessed in a variety of ways in a class:
# public
class Foo
attr_reader :my_var
def initialize(my_var)
@my_var = my_var
end
def do_stuff
puts my_var
end
end
# private
class Foo
def initialize(my_var)
@my_var = my_var
end
private
attr_reader :my_var
def do_stuff
puts my_var
end
end
# direct
class Foo
def initialize(my_var)
@my_var = my_var
end
private
def do_stuff
puts @my_var
end
end
Public attributes should only be used if they are accessed outside of the class. There is not a strong opinion on what strategy is used when attributes are only accessed internally, as long as there is consistency in related code.
In addition to the RuboCop's Layout/EmptyLinesAroundMethodBody and Cop/LineBreakAroundConditionalBlock that enforce some newline styles, we have the following guidelines that are not backed by RuboCop.
# bad
def method
issue = Issue.new
issue.save
render json: issue
end
# good
def method
issue = Issue.new
issue.save
render json: issue
end
# bad
def method
issue = Issue.new
if issue.save
render json: issue
end
end
# good
def method
issue = Issue.new
if issue.save
render json: issue
end
end
# bad
def method
if issue
if issue.valid?
issue.save
end
end
end
# good
def method
if issue
if issue.valid?
issue.save
end
end
end
For ordering methods at the class level (public, protected, private sections), refer to
RuboCop's Layout/ClassStructure.
Within each visibility section, consider the following principles to improve readability:
Methods should generally be ordered from highest to lowest level of abstraction. This means:
This follows the "newspaper style" or "stepdown rule" principle, where code reads like a story from top to bottom, with the most important operations first and implementation details revealed progressively.
# good - orchestrator method before helpers
class CommitMessageProcessor
def execute
other_method_calls
process_commit_message
end
private
def process_commit_message
title = extract_title(commit.message)
body = extract_body(commit.message)
# ... process title and body
end
def extract_title(message)
message.split("\n").first
end
def extract_body(message)
message.split("\n")[1..]
end
end
# bad - helper methods before the method that uses them
class CommitMessageProcessor
def execute
other_method_calls
process_commit_message
end
private
def extract_title(message)
message.split("\n").first
end
def extract_body(message)
message.split("\n")[1..]
end
def process_commit_message
title = extract_title(commit.message)
body = extract_body(commit.message)
# ... process title and body
end
end
This ordering helps readers understand:
Following this ordering pattern helps reviewers and future maintainers understand code flow more quickly, especially in service objects, processors, and other classes with clear orchestration patterns.
Exceptions to this ordering may be appropriate when:
When a class has multiple high-level methods that serve different, unrelated purposes, group each high-level method with its supporting helper methods. Alternatively, consider extracting the implementation into separate service classes where each class has a single clear responsibility.
[!note] Beyond two levels of method calls (a method calling a method calling a method), this pattern can become unwieldy and hard to follow. If you find yourself with deep nesting, consider refactoring into separate classes or simplifying the logic.
This section contains GitLab-specific guidelines for Rails and ActiveRecord usage.
ActiveRecord callbacks allow you to "trigger logic before or after an alteration of an object's state."
Use callbacks when no superior alternative exists, but employ them only if you thoroughly understand the reasons for doing so.
When adding new lifecycle events for ActiveRecord objects, it is preferable to add the logic to a service class instead of a callback.
In general, callbacks should be avoided because:
Some of these examples are discussed in this video from thoughtbot.
The GitLab codebase relies heavily on callbacks and it is hard to refactor them once added due to invisible dependencies. As a result, this guideline does not call for removing all existing callbacks.
Callbacks can be used in special cases. Some examples of cases where adding a callback makes sense:
There is a project with the following basic data model:
class Project
has_one :repository
end
class Repository
belongs_to :project
end
Say we want to create a repository after a project is created and use the project name as the repository name. A developer familiar with Rails might immediately think: sounds like a job for an ActiveRecord callback! And add this code:
class Project
has_one :repository
after_initialize :create_random_name
after_create :create_repository
def create_random_name
SecureRandom.alphanumeric
end
def create_repository
Repository.create!(project: self)
end
end
class Repository
after_initialize :set_name
def set_name
name = project.name
end
end
class ProjectsController
def create
Project.create! # also creates a repository and names it
end
end
While this seems pretty harmless for a baby Rails app, adding this type of logic via callbacks has many downsides once your Rails app becomes large and complex (all of which are listed in this documentation). Instead, we can add this logic to a service class:
class Project
has_one :repository
end
class Repository
belongs_to :project
end
class ProjectCreator
def self.execute
ApplicationRecord.transaction do
name = SecureRandom.alphanumeric
project = Project.create!(name: name)
Repository.create!(project: project, name: name)
end
end
end
class ProjectsController
def create
ProjectCreator.execute
end
end
With an application this simple, it can be hard to see the benefits of the second approach. But we already some benefits:
Repository creation logic separate from Project creation logic. Code
no longer violates law of demeter (Repository class doesn't need to know
project.name).Project and Repository classes.Project factory does not create a second (Repository) object.When creating a new scope, consider the following prefixes.
for_For scopes which filter where(belongs_to: record).
For example:
scope :for_project, ->(project) { where(project: project) }
Timelogs.for_project(project)
with_For scopes which joins, includes, or filters where(has_one: record) or where(has_many: record) or where(boolean condition)
For example:
scope :with_labels, -> { includes(:labels) }
AbuseReport.with_labels
scope :with_status, ->(status) { where(status: status) }
Clusters::AgentToken.with_status(:active)
scope :with_due_date, -> { where.not(due_date: nil) }
Issue.with_due_date
It is also fine to use custom scope names, for example:
scope :undeleted, -> { where('policy_index >= 0') }
Security::Policy.undeleted
order_by_For scopes which order.
For example:
scope :order_by_name, -> { order(:name) }
Namespace.order_by_name
scope :order_by_updated_at, ->(direction = :asc) { order(updated_at: direction) }
Project.order_by_updated_at(:desc)
If a RuboCop rule is proposed and we choose not to add it, we should document that decision in this guide so it is more discoverable and link the relevant discussion as a reference.
Due to the sheer amount of work to rectify, we do not care whether string literals are single or double-quoted.
Previous discussions include:
Individual groups may choose to have an opinion on consistency of quoting styles within the bounded contexts they own, but these decisions only apply to code within that context.
Now that we've upgraded to Ruby 3, we have more options available to enforce type safety.
Some of these options are supported as part of the Ruby syntax and do not require the use of specific type safety tools like Sorbet or RBS. However, we might consider these tools in the future as well.
For now, we can use YARD annotations to define types. IDEs such as RubyMine provide support for YARD when showing type-based inspection errors.
For more information, see Type safety in the remote_development domain README.
Although Ruby and especially Rails are primarily based on object-oriented programming patterns, Ruby is a very flexible language and supports functional programming patterns as well.
Functional programming patterns, especially in domain logic, can often result in more readable, maintainable, and bug-resistant code while still using idiomatic and familiar Ruby patterns.
However, functional programming patterns should be used carefully because some patterns would cause confusion and should be avoided even if they're directly supported by Ruby. The curry method is a likely example.
For more information, see: