Architecture Overview
This document explains the architectural design of the monday_ruby gem, exploring how its components work together and why they're designed this way.
Table of Contents
- Overall Architecture
- The Client-Resource Pattern
- Dynamic Resource Initialization
- GraphQL Query Building
- Request-Response Flow
- Error Handling Architecture
- Alternative Approaches
Overall Architecture
The monday_ruby gem is built around a client-resource architecture that wraps the monday.com GraphQL API in Ruby classes. Rather than exposing raw GraphQL queries or providing REST-style endpoints, the gem presents a resource-oriented interface that feels natural to Ruby developers.
The architecture consists of three main layers:
- Client Layer - The entry point that handles authentication, configuration, and HTTP communication
- Resource Layer - Domain-specific classes for each monday.com resource (Board, Item, Group, etc.)
- Utility Layer - Shared components for GraphQL query construction, error handling, and HTTP requests
This layered approach creates clear separation of concerns: resources focus on domain logic and query construction, the client handles communication, and utilities provide shared functionality.
The Client-Resource Pattern
Why This Pattern?
The client-resource pattern emerged from several design goals:
1. Encapsulation of GraphQL Complexity
monday.com's API is built on GraphQL, which is powerful but verbose. While GraphQL gives developers fine-grained control over data fetching, it requires understanding schema structure, query syntax, and field relationships. The client-resource pattern shields users from this complexity.
Instead of writing:
query = "query{boards(ids: [123]){id name description items{id name}}}"
# Now figure out how to execute this...Users work with Ruby methods:
client = Monday::Client.new(token: "...")
client.board.query(args: {ids: [123]}, select: ["id", "name", "description"])2. Natural Organization by Domain
monday.com has distinct concepts: boards, items, groups, columns, updates, workspaces, etc. By creating a resource class for each concept, the gem mirrors monday.com's domain model. Users can discover functionality by exploring resources that match their mental model of monday.com.
3. Shared Infrastructure
All resources share common needs: authentication, error handling, HTTP communication, and query building. The client-resource pattern allows resources to inherit shared behavior from Resources::Base while delegating infrastructure concerns to the client.
How It Works
The pattern involves two main components:
The Client (/Users/sanifhimani/Development/monday_ruby/lib/monday/client.rb)
The client is initialized with configuration and creates instances of all resource classes:
def initialize(config_args = {})
@config = configure(config_args)
Resources.initialize(self)
endThe client provides:
- Configuration management (tokens, endpoints, timeouts)
- Request execution (
make_request) - Response handling and error raising
- HTTP header construction
Resources (/Users/sanifhimani/Development/monday_ruby/lib/monday/resources/)
Each resource class inherits from Resources::Base and receives a reference to the client:
class Base
attr_reader :client
def initialize(client)
@client = client
end
protected
def make_request(query)
client.make_request(query)
end
endResources build GraphQL queries and delegate execution to the client. For example, Board#query:
def query(args: {}, select: DEFAULT_SELECT)
request_query = "query{boards#{Util.format_args(args)}{#{Util.format_select(select)}}}"
make_request(request_query)
endThis separation means resources focus on "what" to query, while the client handles "how" to execute it.
Dynamic Resource Initialization
One of the most interesting aspects of the architecture is how resources are automatically discovered and attached to the client.
The Mechanism
The Resources module (/Users/sanifhimani/Development/monday_ruby/lib/monday/resources.rb) uses Ruby metaprogramming to dynamically initialize resources:
def self.initialize(client)
constants.each do |constant|
resource_class = const_get(constant)
resource_name = constant.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
client.instance_variable_set("@#{resource_name}", resource_class.new(client))
define_resource_accessor(client, resource_name) unless client.class.method_defined?(resource_name)
end
endThis code:
- Iterates over all constants in the
Resourcesmodule (which are the resource classes) - Converts class names to snake_case (
Board→board,BoardView→board_view) - Creates an instance of each resource, passing the client
- Sets it as an instance variable on the client
- Defines an attr_reader for accessing the resource
Why This Approach?
This dynamic initialization provides several benefits:
1. Automatic Registration
Adding a new resource is as simple as creating a file in lib/monday/resources/. The file is automatically loaded (via Dir[File.join(__dir__, "resources", "*.rb")].sort.each { |file| require file }), and the resource becomes available on the client. No manual registration needed.
2. Consistent Interface
All resources follow the same naming convention automatically. Board becomes client.board, ActivityLog becomes client.activity_log. Developers can predict the accessor name from the class name.
3. Single Responsibility
Resources don't need to know about initialization logic. They simply inherit from Base and implement their methods. The Resources module handles the metaprogramming.
4. Extensibility
Third-party code could extend the Resources module with additional classes, and they would automatically be initialized and available on the client.
Trade-offs
This approach has some trade-offs:
- Implicit Behavior: The client's interface isn't explicitly defined in code. You must look at the resources directory to know what methods are available.
- IDE Support: Some IDEs struggle with dynamically defined methods, making autocomplete harder.
- Debugging: Metaprogramming can make stack traces more complex.
However, the benefits of automatic registration and consistency outweigh these concerns for a library like this, where resources map cleanly to domain concepts.
GraphQL Query Building
The gem translates Ruby method calls into GraphQL queries through the Util class (/Users/sanifhimani/Development/monday_ruby/lib/monday/util.rb).
The Challenge
GraphQL has specific syntax requirements:
query {
boards(ids: [123, 456], limit: 10) {
id
name
items {
id
name
}
}
}The gem needs to convert Ruby hashes and arrays into this format while handling:
- Arguments:
{ids: [123, 456]}→(ids: [123, 456]) - Field selection:
["id", "name", {"items" => ["id"]}]→id name items { id } - Value formatting: strings need quotes, integers don't, symbols are literals
- Nested structures: arrays within hashes, hashes within arrays
The Solution
The Util class provides two main methods:
Util.format_args - Converts Ruby hashes to GraphQL arguments:
Util.format_args({board_name: "My Board", workspace_id: 123})
# => "(board_name: \"My Board\", workspace_id: 123)"It handles:
- String escaping (wrapping strings in quotes)
- Integer preservation (no quotes)
- JSON encoding for complex hashes (double-encoded for monday.com's API)
- Array formatting
- Symbol pass-through (for GraphQL enum values)
Util.format_select - Converts Ruby arrays/hashes to field selection:
Util.format_select(["id", "name", {"items" => ["id", "name"]}])
# => "id name items { id name }"It recursively processes arrays and hashes to create nested field selections.
Why String Building?
The gem builds queries as strings rather than using a GraphQL client library like graphql-client. This design choice has important implications:
Advantages:
- Simplicity: No external GraphQL dependencies, just string manipulation
- Transparency: Generated queries are easy to debug (just print the string)
- Flexibility: Can support any GraphQL feature monday.com adds without library updates
- Performance: No query parsing or AST construction overhead
Disadvantages:
- No validation: Malformed queries aren't caught until the API responds
- String escaping: Must manually handle special characters and injection risks
- No type safety: Can't verify field names or argument types at build time
For this gem, simplicity and transparency outweigh validation benefits. The monday.com API provides clear error messages when queries are malformed, and the gem's error handling surfaces these to users.
Request-Response Flow
Understanding how requests flow through the system illuminates the architecture's design.
The Complete Flow
User calls a resource method
rubyclient.board.query(args: {ids: [123]}, select: ["id", "name"])Resource builds a GraphQL query string
ruby# Inside Board#query request_query = "query{boards(ids: [123]){id name}}"Resource calls
make_request(inherited from Base)rubymake_request(request_query) # Delegates to client.make_request(request_query)Client executes the HTTP request
ruby# Inside Client#make_request response = Request.post(uri, body, request_headers, ...)Request.post wraps Net::HTTP
ruby# Inside Request.post http = Net::HTTP.new(uri.host, uri.port) request = Net::HTTP::Post.new(uri.request_uri, headers) request.body = {"query" => query}.to_json http.request(request)Response is wrapped in Monday::Response
ruby# Back in Client#make_request handle_response(Response.new(response))Client checks for errors
ruby# Inside Client#handle_response return response if response.success? raise_errors(response)Response returned to user
rubyresponse.body # => {"data" => {"boards" => [...]}}
Why This Flow?
This multi-step flow might seem complex, but each layer has a purpose:
Resources: Know the domain and GraphQL schema structure Client: Manages authentication and error handling Request: Abstracts HTTP details Response: Provides a consistent interface to raw HTTP responses
This separation allows each component to change independently. For example:
- Resources can be added without changing the client
- The HTTP library could be swapped without touching resources
- Error handling logic is centralized in one place
Error Handling Architecture
monday.com's API returns errors in multiple ways, and the gem's error handling architecture deals with this complexity.
The Problem
Errors can appear as:
- HTTP status codes: 401, 403, 404, 429, 500
- Error codes in the response body:
ComplexityException,InvalidBoardIdException, etc. - GraphQL errors array:
{"errors": [{"message": "...", "extensions": {"code": "..."}}]}
Additionally, monday.com returns HTTP 200 for some errors (like malformed GraphQL queries), with error details in the response body.
The Solution
The gem uses a two-tier error detection system:
Tier 1: HTTP Status Codes (Client#default_exception)
def default_exception(response)
Util.status_code_exceptions_mapping(response.status).new(response: response)
endMaps status codes to exception classes:
- 401/403 →
AuthorizationError - 404 →
ResourceNotFoundError - 429 →
RateLimitError - 500 →
InternalServerError
Tier 2: Response Body Error Codes (Client#response_exception)
def response_exception(response)
error_code = response_error_code(response)
exception_klass, code = Util.response_error_exceptions_mapping(error_code)
exception_klass.new(message: error_code, response: response, code: code)
endExtracts error codes from multiple possible locations:
response.body["error_code"]response.body.dig("errors", 0, "extensions", "code")response.body.dig("errors", 0, "extensions", "error_code")
Then maps them to specific exceptions like ComplexityError, InvalidBoardIdException, etc.
Success Detection
The Response#success? method combines both checks:
def success?
(200..299).cover?(status) && !errors?
end
def errors?
(parse_body.keys & ERROR_OBJECT_KEYS).any?
endA response is only successful if:
- HTTP status is 2xx
- Response body doesn't contain
errors,error_code, orerror_messagekeys
This handles monday.com's quirk of returning 200 for GraphQL errors.
Exception Hierarchy
All exceptions inherit from Monday::Error, allowing users to rescue all monday.com errors with a single rescue clause:
begin
client.board.query(...)
rescue Monday::Error => e
puts "API error: #{e.message}"
puts "Response: #{e.response.body}"
endSpecific exceptions allow targeted error handling:
rescue Monday::RateLimitError => e
sleep(60)
retry
rescue Monday::AuthorizationError => e
refresh_token
retryWhy This Complexity?
The error handling is complex because it must handle:
- monday.com's evolving API (new error codes appear)
- Multiple error code formats (monday.com changed formats over time)
- HTTP vs. application-level errors
- User expectations (want specific exception types, but also a catch-all)
The architecture provides specificity when needed, but gracefully degrades to generic Monday::Error for unknown error codes.
Alternative Approaches
To understand why monday_ruby is designed this way, it's useful to consider alternatives.
Alternative 1: Direct GraphQL Client
The gem could expose a thin wrapper around a GraphQL client:
client = Monday::Client.new(token: "...")
query = <<~GRAPHQL
query {
boards(ids: [123]) {
id
name
}
}
GRAPHQL
response = client.execute(query)Trade-offs:
- ✅ Maximum flexibility - can use any GraphQL feature
- ✅ Familiar to GraphQL users
- ❌ Requires GraphQL knowledge
- ❌ Verbose for common operations
- ❌ No Ruby-idiomatic interface
- ❌ Error handling less structured
This approach is better for users who already know GraphQL and want full control. monday_ruby prioritizes ease of use for Ruby developers who may not know GraphQL.
Alternative 2: REST-Style Interface
The gem could mimic REST APIs:
client = Monday::Client.new(token: "...")
boards = client.get("/boards", params: {ids: [123]})
item = client.post("/items", body: {name: "New Item", board_id: 456})Trade-offs:
- ✅ Familiar to REST API users
- ✅ Simple HTTP-style interface
- ❌ Doesn't match monday.com's GraphQL nature
- ❌ Hard to represent nested field selection
- ❌ Inefficient data fetching (over-fetching or under-fetching)
This works well for REST APIs but fights against GraphQL's strength of precise data fetching.
Alternative 3: Active Record Pattern
The gem could model monday.com resources as ActiveRecord-style objects:
board = Monday::Board.find(123)
board.name = "New Name"
board.save
item = board.items.create(name: "New Item")Trade-offs:
- ✅ Familiar to Rails developers
- ✅ Object-oriented interface
- ❌ Implies more than the API provides (no change tracking, associations, etc.)
- ❌ Requires maintaining object state
- ❌ Hides GraphQL's explicit field selection
- ❌ More complex implementation
While appealing, this pattern doesn't fit monday.com's API model. The API is query-based, not CRUD-based, and doesn't support partial updates or lazy loading the way ActiveRecord expects.
The Chosen Approach
monday_ruby's client-resource pattern strikes a balance:
- More accessible than raw GraphQL (Ruby methods vs. query strings)
- More powerful than REST-style (explicit field selection, complex queries)
- Simpler than ActiveRecord (stateless, explicit requests)
- Flexible enough to support monday.com's evolving API
The design prioritizes:
- Ease of use for developers new to monday.com or GraphQL
- Explicit behavior over magic (you call methods, get responses)
- Extensibility (easy to add resources and methods)
- Transparency (clear what queries are being executed)
Conclusion
The monday_ruby architecture emerged from practical constraints and design goals:
- Client-Resource Pattern: Organizes code around monday.com's domain model
- Dynamic Resource Initialization: Enables automatic registration and consistent naming
- String-Based Query Building: Balances simplicity with flexibility
- Layered Request Flow: Separates concerns while maintaining clarity
- Robust Error Handling: Deals with monday.com's varied error formats
These decisions create a gem that feels natural to Ruby developers while effectively wrapping a complex GraphQL API. The architecture isn't the only way to solve this problem, but it's well-suited to the gem's goals of accessibility, maintainability, and power.