Open-Source Wikis

/

GitLab

/

How to contribute

/

Patterns and conventions

gitlab-org/gitlab

Patterns and conventions

The shared idioms you'll see throughout the codebase. Following these is required for MR review to go smoothly.

Service objects

Almost all business logic lives in app/services/. The base classes are:

  • BaseService (app/services/base_service.rb) — generic.
  • BaseContainerService (app/services/base_container_service.rb) — for things that act on a project / group / namespace.
  • BaseProjectService, BaseGroupService — sugar for the above.
  • IssuableBaseService — issues / merge requests.

A typical service:

module Projects
  class CreateService < BaseService
    def execute
      return ServiceResponse.error(message: 'Forbidden') unless authorized?

      project = build_project
      return ServiceResponse.error(message: errors_for(project)) unless project.save

      after_create(project)
      ServiceResponse.success(payload: { project: project })
    end
    # ...
  end
end

Conventions:

  • #execute is the public entry point and returns a ServiceResponse.
  • Initialization parameters are explicit; no magic context.
  • Database mutations happen inside the service, not in the controller.
  • A service may enqueue Sidekiq jobs via MyWorker.perform_async or domain events via Gitlab::EventStore.publish.

ServiceResponse (app/services/service_response.rb) carries:

  • status:success or :error.
  • message — human-readable.
  • payload — domain object(s).
  • reason — machine-readable error symbol used by GraphQL/REST mappers.
  • http_status — HTTP code hint.

Finders

Read queries with permission filtering go in app/finders/:

class IssuesFinder
  def initialize(current_user, params = {})
    # ...
  end

  def execute
    # returns an ActiveRecord::Relation
  end
end

Finders never mutate, never call Sidekiq, and always apply ability checks. They're heavily reused by controllers, services, and GraphQL resolvers.

Authorization with declarative_policy

Permissions live in app/policies/:

class ProjectPolicy < ::BasePolicy
  condition(:owner) { @user&.id == @subject.creator_id }
  condition(:reporter) { team_access?(:reporter) }

  rule { reporter | owner }.enable :read_project
end

In a controller or service:

return ServiceResponse.error(message: 'Forbidden') unless can?(current_user, :read_project, project)

The declarative_policy gem caches conditions per request and is heavily optimized.

EE prepend pattern

Enterprise-only behavior lives in ee/. The prepend_mod_with macro wires a CE class to its EE module:

# app/services/projects/create_service.rb
module Projects
  class CreateService < BaseService
    def execute
      # ...
    end
  end
end

Projects::CreateService.prepend_mod_with('Projects::CreateService')
# ee/app/services/ee/projects/create_service.rb
module EE
  module Projects
    module CreateService
      extend ::Gitlab::Utils::Override

      override :execute
      def execute
        super.tap do |response|
          # EE-only side effects
        end
      end
    end
  end
end

Always use override and super so future refactors stay safe.

Bounded contexts

config/bounded_contexts.yml defines the allowed top-level Ruby namespaces. New code must live in one (e.g. Ci::, MergeRequests::, Authn::). The Gitlab/BoundedContexts cop enforces this.

Application-layer code (controllers, REST endpoints, views) is exempt.

Feature categories

Every controller, service, worker, and gem must declare a feature category from config/feature_categories.yml:

# Controller
class ProjectsController < ApplicationController
  feature_category :groups_and_projects
end

# Worker
class FooWorker
  include ApplicationWorker
  feature_category :continuous_integration
end

# RSpec
describe Foo, feature_category: :continuous_integration do
end

# Gem
gem 'rouge', feature_category: :source_code_management

The cop Gitlab::FeatureCategory blocks merging without one.

Domain events with EventStore

For decoupled side effects, publish events instead of directly invoking workers:

# Producer
Gitlab::EventStore.publish(
  Projects::ProjectCreatedEvent.new(data: { project_id: project.id })
)

# Consumer (subscribed in a Sidekiq worker)
class MyWorker
  include Gitlab::EventStore::Subscriber
  feature_category :continuous_integration

  def handle_event(event)
    # ...
  end
end

Events are defined under app/events/ and ee/app/events/. See lib/gitlab/event_store.rb.

Idempotent and deduplicated jobs

Default for new Sidekiq workers:

class MyWorker
  include ApplicationWorker
  idempotent!
  deduplicate :until_executed
  data_consistency :always
  urgency :low
  feature_category :foo
end

The cop Sidekiq/IdempotentWorker flags missing idempotent!.

Database conventions

  • All tables have a created_at and updated_at.
  • Foreign keys are added via add_concurrent_foreign_key in migrations (no add_foreign_key directly).
  • Indexes use add_concurrent_index.
  • Loose foreign keys for cross-DB references go in config/gitlab_loose_foreign_keys.yml.
  • The :bigint rule: every primary key is bigint. The db/integer_ids_not_yet_initialized_to_bigint.yml lists exceptions still on the way.

Error handling

  • Exceptions inherit from Gitlab::Error::* in lib/gitlab/error/.
  • Don't rescue Exception; rescue specific classes.
  • Use Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) to report to Sentry while raising in dev/test.
  • Use Gitlab::ApplicationContext.with_context(user: user, project: project) to attach metadata to errors.

Logging

Domain-specific loggers live in lib/gitlab/:

  • Gitlab::AppLogger
  • Gitlab::AppJsonLogger
  • Gitlab::AuditJsonLogger
  • Gitlab::DatabaseWarningsLogger
  • many more

In production logs are JSON; in development they're text. See Logging.

Tagging methods with feature_category and metadata

Workers can declare:

  • urgency :high (low / medium / high)
  • data_consistency :always (always / sticky / delayed)
  • worker_resources :cpu_bound
  • concurrency_limit -> { ... } for adaptive throttling.

Controllers and Grape endpoints declare urgency :high similarly via Gitlab::EndpointAttributes.

i18n

User-facing strings flow through _('...') (gettext). Translations live in locale/<lang>/gitlab.po. The cop Gitlab::I18N enforces the format.

Frontend conventions

  • Vue 2 → Vue 3 migration in progress; config/vue3migration/ tracks state.
  • Composables and <script setup> allowed in new components.
  • GraphQL via Apollo; wire mutations via apollo.mutate and queries via apollo.query.
  • Vuex stores in app/assets/javascripts/<feature>/store/. Pinia is approved for new stores.
  • GitLab UI components from the @gitlab/ui package; do not roll your own buttons.
  • Tailwind utility classes are allowed; tokens defined in config/tailwind.config.js.

Naming

  • Files: snake_case.rb for Ruby, kebab-case.vue for Vue components.
  • Classes / modules: CamelCase.
  • Constants: SCREAMING_SNAKE_CASE.
  • DB columns: snake_case.
  • GraphQL types: CamelCase, fields camelCased on the GraphQL side, snake_cased server-side.
  • Tooling — what enforces these conventions.
  • Testing — how to verify your conventions are correct.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – GitLab wiki | Factory