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
endConventions:
#executeis the public entry point and returns aServiceResponse.- 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_asyncor domain events viaGitlab::EventStore.publish.
ServiceResponse (app/services/service_response.rb) carries:
status—:successor: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
endFinders 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
endIn 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
endAlways 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_managementThe 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
endEvents 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
endThe cop Sidekiq/IdempotentWorker flags missing idempotent!.
Database conventions
- All tables have a
created_atandupdated_at. - Foreign keys are added via
add_concurrent_foreign_keyin migrations (noadd_foreign_keydirectly). - Indexes use
add_concurrent_index. - Loose foreign keys for cross-DB references go in
config/gitlab_loose_foreign_keys.yml. - The
:bigintrule: every primary key isbigint. Thedb/integer_ids_not_yet_initialized_to_bigint.ymllists exceptions still on the way.
Error handling
- Exceptions inherit from
Gitlab::Error::*inlib/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::AppLoggerGitlab::AppJsonLoggerGitlab::AuditJsonLoggerGitlab::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_boundconcurrency_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.mutateand queries viaapollo.query. - Vuex stores in
app/assets/javascripts/<feature>/store/. Pinia is approved for new stores. - GitLab UI components from the
@gitlab/uipackage; do not roll your own buttons. - Tailwind utility classes are allowed; tokens defined in
config/tailwind.config.js.
Naming
- Files:
snake_case.rbfor Ruby,kebab-case.vuefor 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.
Related
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.