Domain Driven Design with GraphQL on Ruby on Rails API

Paul-Armand Assus
4 min readFeb 14, 2018

--

Hello everyone,

For the past year, at A line, we had to build an ambitious project management and marketplace platform MVP from scratch. The frontend is built in TypeScript/React/Apollo (Charly Poly).

For the backend, Charly and I had three goals :

  • Implementing DDD in Rails
  • Implementing a GraphQL API with WebSocket subscriptions
  • Having a well-tested, maintainable, and scalable codebase

FYI, today it’s all up and running in production on A Line, the Startup I work on.

The gems

Basic rails/puma/pg/sidekiq/redis. For authentication we choose to use a very pleasant API called Auth0, that provide JWT authentication. For search we use Algolia. Of course, for graphql, the graphql-ruby gem.

The core Architecture

Let’s get an example with the model Portfolio. I’ll not go into subscriptions, but it works great with ActionCable (next article ?).

app/
app/admin/ # ActiveAdmin
app/models/ # The models (User/Portfolio ....)
app/controllers/ # Only for the GraphQL controller
app/graphql/ # The GraphQL code
app/graphql/aline_api_schema.rb # Main graphql schema
app/graphql/mutation_type.rb # Defines all mutations
app/graphql/query_type.rb # Defines all queries
app/graphql/subscriptions_type.rb # Defines the subscriptions
app/graphql/resource_base_service.rb
# Base service object that exposes the index/show/create/update/destroy basic methods that will be called by the mutations
app/graphql/portfolios/ # Portfolio domain
app/graphql/portfolios/type.rb
# Portfolio GraphQL type
app/graphql/portfolios/mutations/
app/graphql/portfolios/mutations/create.rb
# Create mutation
app/graphql/portfolios/mutations/update.rb
# Update mutation
app/graphql/portfolios/mutations/accept.rb
# Accept mutation
app/graphql/portfolios/mutations/input_type.rb
# Portfolio input
app/graphql/portfolios/service.rb
# Service Object that may surcharge the index/show/create... methods inherited by resource_base_service methods called by the mutations, and add the accept method.

The code

The controller

The GraphQL controller is pretty simple, just calling the graphql schema.

app/controllers/graphql_controller.rbclass GraphqlController < ApplicationController  before_action :authenticate_user  # GraphQL endpoint
def execute
result = AlineApiSchema.execute(
params[:query],
variables: ensure_hash(params[:variables]),
context: { current_user: current_user },
operation_name: params[:operationName]
)
render json: result
end
private
def ensure_hash(ambiguous_param)
case ambiguous_param
when String
ambiguous_param.present? ? ensure_hash(JSON.parse(ambiguous_param)) : {}
when Hash, ActionController::Parameters
ambiguous_param
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
end
end
end

The Service Object part

the ResourceBaseService (simplified) looks like that :

app/graphql/resource_base_service.rbclass ResourceBaseService  attr_accessor :params, :object, :user  def initialize(params: {}, object: nil, object_id: nil, user: nil)
@params = params.to_h.symbolize_keys
@object = object || model.find_by(id: object_id)
@user = user
end
def self.graphql_call(resource, method)
lambda { |_obj, args, ctx|
params = args.try(:[], resource.to_sym) || args
service = "{resource.to_s.pluralize.camelize}::Service"
service.constantize.new(
params: params,
user: ctx[:current_user],
object_id: args[:id]
).send(method)
}
end
def index
# Example of a index if ApplicationRecord defines the
# self.seeable_by method
model.seeable_by(user: user)
end
def show
# Example of a show if ApplicationRecord defines the
# self.seeable_by method
return not_allowed unless model.seeable_by(user: user).include?(object)
object
end
def create
# Creates the record regarding params and authorizations
model.create!(params)
end
def destroy
# Destroys the record regarding authorizations
object.destroy!
end
def update
# Updates the record regarding params and authorizations
object.update_attributes!(params)
end
private

def singular_resource
resource_name.singularize
end
def model
singular_resource.camelize.constantize
end
def resource_name
self.class.to_s.split(':').first.underscore
end
end

Then you have the Portfolios Service

app/graphql/portfolios/service.rbmodule Portfolios
class Service < ResourceBaseService
# Custom Mutation
def accept
return unless model.writable_by(user: user).include?(object)
object.tap(&:accept!)
end
# Add action after create
def create
super
# add fancy stuff
end
# Rewritten index
def index
model.where(visible: true).seeable_by(user: user)
end
end
end

The GraphQL part

The Portfolio Type

app/graphql/portfolios/type.rbPortfolios::Type = GraphQL::ObjectType.define do
name 'Portfolio'
field :id, types.ID # types.String for uuid mapping
field :visible, types.Bool
field :user_id, types.ID
field :created_at, types.String
field :updated_at, types.String
end

And the mutations, first the input type, you can have many :

app/graphql/portfolios/mutations/input_type.rbPortfolios::Mutations::InputType = GraphQL::InputObjectType.define do  name 'Portfolio input type'  argument :visible, types.Bool
argument :user_id, types.ID
end

and for example the update mutation :

app/graphql/portfolios/mutations/update.rbPortfolios::Mutations::Update = GraphQL::Field.define do  description 'Updates a Portfolio'  type Portfolios::Type
argument :id, !types.ID
argument :portfolio, Portfolios::Mutations::InputType # The input type defined earlier
resolve ResourceBaseService.graphql_call(:portfolio, :update)
end

Then simply add them to the mutations_type :

app/graphql/mutation_type.rbMutationType = GraphQL::ObjectType.define do  name 'Mutation'  field :update_portfolio, Portfolios::Mutations::Update
# ...
end

And add the queries to the query_type :

app/graphql/query_type.rbQueryType = GraphQL::ObjectType.define do  field :portfolio do
type !Portfolios::Type
description 'Return a Portfolio'
argument :id, !types.ID
resolve ResourceBaseService.graphql_call(:portfolios, :show)
end
field :portfolios do
type !types[!Portfolios::Type]
description 'Return the Portfolios'
resolve ResourceBaseService.graphql_call(:portfolios, :index)
end
# ... Metaprog could be your friend here.end

finally the graphql schema

app/graphql/aline_api_schema.rbAlineApiSchema = GraphQL::Schema.define do
mutation(MutationType)
query(QueryType)
use GraphQL::Subscriptions::ActionCableSubscriptions
subscription(SubscriptionType)
end

OMG, that’s it, you have your graphql API. So fu**ing good.

Conclusion

The Service Objects part is maybe not the best thing for all projects, but here it works pretty well.

You can unit test all your services methods, and for me that’s the only real things to test.

For N+1 queries, I suggest you to tweak your index methods with model.includes() regarding the context ast nodes, or take a look tographql-batch, another wonderful gem made by Shopify ❤.

The performances are quite good for the moment. GraphQL is a wonderful tool to build APIs, and in ruby it’s pretty pleasant right now :)

Hope it helps, don’t hesitate to reach me for more details. By the way, if you are a talented freelance and look for quality projects, take a look at A line.

--

--