Beautiful Abstractions

Aesthetic code that reads like poetry, but poets seem to die young.

Author profile picture

@caifanuncle

Feb 2025

Before joining the public service where I now work primarily with Typescript, I originally cut my teeth as a software engineer at a fintech company that runs on Ruby, mostly on Rails.

Owing to the beautiful syntax and expressive power of Ruby, I had the joy of reading and writing code that often flowed like poetry. This essay features a few samples of beautiful abstractions which are possible with Ruby. In true poetic fashion, this dedication also ends on a slightly sombre note. Or does it?

Service Objects

Service objects are a pattern that encapsulates business logic into discrete, reusable units. Here's a plausible implementation:

# app/services/application_service.rb # require 'dry-initializer' require 'dry/monads/all' class ApplicationService extend Dry::Initializer include Dry::Monads def self.call(**arguments) result = new(**arguments).() yield result if block_given? result end end

We leverage the dry-rb ecosystem to create a consistent interface for business operations, with elegant and predictable error handling through result monads.

# app/services/bookings/create.rb # module Bookings class Create < ApplicationService option :customer_id option :staff_id option :starts_at option :remarks, optional: true option :duration, default: -> { 30 } option :services, default: -> { [] } def call ActiveRecord::Base.transaction do if timeslot_available? && !staff_rest_day? create_booking! update_timeslot_availability! else return Failure("Unable to create booking") end end if booking.persisted? Success(booking) else Failure("Unable to create booking") end rescue StandardError => e Rails.logger.error("Unable to create booking: #{e.message}") Failure("Unable to create booking: #{e.message}") end private def staff_rest_day? weekday = starts_at.strftime('%A').downcase !staff.data.dig('workdays', weekday) end def timeslot_available? timeslot_availability.available?(timestamps_to_book) end def timestamps_to_book timestamps = [] timestamps << starts_at ((duration / 30) - 1).times do timestamps << timestamps.last + 30.minutes end timestamps.map { (_1.to_f * 1000).to_i } end def update_timeslot_availability! timeslot_availability.update!( data: timeslot_availability.data.merge({ "booked_timestamps" => timeslot_availability.data["booked_timestamps"] + timestamps_to_book }) ) end def timeslot_availability @timeslot_availability ||= TimeslotAvailability.find_or_create_by( staff_id:, date: starts_at ).lock! end def booking @booking ||= Booking.create!( # omitted for brevity ) end def staff @staff = Staff.find(staff_id) end def customer @customer = Customer.find(customer_id) end alias_method :create_booking!, :booking end end # sample usage # Bookings::Create.( customer_id: 1, staff_id: 1, starts_at: Time.now + 1.hour, duration: 30, services: ["service_1", "service_2"] )

Concerns

Concerns in Rails allow you to easily extract composable logic into easy-to-maintain modules with some nice synthetical sugar. Here are some examples of how concerns might be used to keep controllers and models clean.

Param Validation

When it comes to parameter validation for incoming requests, I like how JS libraries like Zod and Joi allow you to define schemas for your parameters in a way that is granular yet easy to work with. I find this more useful than the Rails default which would involve writing something like params.require(:post).permit(:title, :body).

Tapping onto dry-rb again, we can easily emulate a similar interface in Ruby.

# app/controllers/concerns/parameter_validation.rb # module ParameterValidation extend ActiveSupport::Concern included do class_attribute :parameter_schemas, instance_writer: false, default: {} before_action :validate_parameters, prepend: true end class_methods do def params_for(action, schema = nil, &block) schema ||= Dry::Schema.Params(&block) parameter_schemas["#{controller_path}##{action}"] = schema end end def params if validated_parameters ActionController::Parameters.new(validated_parameters).permit! else super end end private attr_reader :validated_parameters def validate_parameters schema = parameter_schemas["#{controller_path}##{action_name}"] return if schema.blank? validation = schema.(request.params) if validation.success? @validated_parameters = validation.to_h.merge( { "authenticity_token" => request.params[:authenticity_token], "page" => request.params.dig(:pagination, :page), "page_size" => request.params.dig(:pagination, :page_size), } ) else render json: { errors: validation.errors.to_h }, status: 403 end end end

The above concern allows you to define schemas for your parameters in a way that is both expressive and easy to understand.

# app/controllers/application_controller.rb # class ApplicationController < ActionController::Base include ParamValidation end # app/controllers/posts_controller.rb # class PostsController < ApplicationController params_for :create do required(:post).schema do required(:title).filled(:string) required(:body).filled(:string) end end def create post = Post.new( title: params[:post][:title], body: params[:post][:body], user_id: current_user.id ) if post.save render json: post, status: :created else render json: post.errors, status: :unprocessable_entity end end end

(Admittedly, this doesn't have the code completion ergonomics that something like Vine provides through Typescript's type system.)

Soft Deletes

Soft deletes are a common pattern in software applications, especially since the app stores require "account deletion (*wink* *wink*)" to be an option for users... An annoying requirement, but nothing that a simple "deleted_at" column and a few lines of Ruby can't solve. For legal reasons, that's a joke.

# app/models/concerns/soft_deletable.rb # module SoftDeletable extend ActiveSupport::Concern included do scope :active, -> { where(deleted_at: nil) } scope :deleted, -> { where.not(deleted_at: nil) } scope :with_deleted, -> { unscope(where: :deleted_at) } default_scope -> { active } end def deleted? deleted_at.present? end def active? !deleted? end def soft_delete update_column(:deleted_at, Time.current) end def restore update_column(:deleted_at, nil) end def destroy raise ActiveRecord::RecordNotDestroyed, "Cannot destroy soft deletable record" end end class User < ApplicationRecord include SoftDeletable end

So simple. So elegant.

Response Pipeline

Now for a more involved example! Imagine a Rails concern that standardizes API response handling across a Rails application by providing a unified serialization and pagination interface.

# app/controllers/posts_controller.rb # class PostsController < ApplicationController # NOTE: The following ResponsePipeline module is intended to be included in # ApplicationController to standardise the response format across the app include ResponsePipeline def index posts = Post.all render json: serialize(posts) end def show post = Post.find(params[:id]) render json: serialize(post) end def filtered_index posts = Post.where(published: true) # Custom serializer option render json: serialize(posts, serializer: PublishedPostSerializer) end def with_comments post = Post.find(params[:id]) # Pass additional options to serializer render json: serialize(post, include_comments: true) end end # app/controllers/concerns/response_pipeline.rb # module ResponsePipeline extend ActiveSupport::Concern included do before_action :set_pagination_params end private def set_pagination_params @page = (params[:page] || 1).to_i @per_page = (params[:per_page] || 25).to_i @per_page = 100 if @per_page > 100 # Limit maximum per_page end def serialize(records, options = {}) if records.respond_to?(:to_a) paginated_records = paginate(records) serialized_data = paginated_records.map { |record| serialize_record(record, options) } { data: serialized_data, meta: { current_page: @page, per_page: @per_page, total_pages: (records.count.to_f / @per_page).ceil, total_count: records.count } } else serialize_record(records, options) end end def paginate(records) records.limit(@per_page).offset((@page - 1) * @per_page) end def serialize_record(record, options = {}) serializer_class = options[:serializer] || "#{record.class.name}Serializer".constantize serializer_class.new(record, options.except(:serializer)).as_json rescue NameError => e # Fallback to simple as_json if serializer not found record.as_json(options) end end

It automatically handles both single-record and collection responses, applies consistent pagination, manages serializer selection, and structures the response format to include both the serialized data and metadata for collections.

This centralization helps maintain consistency across endpoints, a powerful and useful abstraction indeed.

The above examples barely scratch the surface of what is possible with Ruby! Ruby pushes metaprogramming and object oriented design in a way that allows you to craft tailored abstractions for your business logic and domain. We often associate Ruby with it being an expressive programming language, but what does expressiveness actually mean? Personally, it is the essence of being able to capture a wide surface area of business logic and context with very little code and boilerplate. It is about conveying a lot, with very little. It is about compressing complexity.

A Dying Art?

We know every programming language has its own strengths and weaknesses. However, the nuances in how to choose and evaluate between them have interestingly shifted in our current prompt-ous world.

Writing Typescript feels safe, but writing Ruby resonates better. While I used to prioritise writing code that feels good (ie Ruby), I now find myself slowly gravitating towards safety features since more and more of my code is being generated by LLMs.

This is an interesting thought; which is that programming languages and their evolution in values and design have to be LLM-first moving forward (eg. static-typed languages now bring more value, and rejecting dynamic-typed languages can no longer be attributed to just 'skill-issue'). The technical craft of writing application code might feel less inspiring and more mechanical, but it will be what works.

It makes me wonder about Typescript as the default programming language for the web, and whether or not that is desirable. Recently, I developed interest in the BEAM ecosystem, and picked up Gleam in the process. After building a few tiny web services with it, I'm convinced it is one of the best programming languages to ever be designed. I'll not go into why I believe so, but consider this: It is perhaps now difficult for a new programming language to gain popularity because LLM-driven development favours languages that are already popular, eg Typescript. Gleam has a brilliant LSP that enhances the developer experience way more than Typescript's (for me), but the lack of LLM auto-completion made it feel like I was taking 1 step forward, but 2 steps back.

There is still much room for improvements and evolution in programming language design, but LLMs might make it harder for new ideas to gain traction. However, in a world where LLMs are starting to become the largest code contributors, does it really matter?

On Second Thought...

I do see how things might potentially go the other way though. Perhaps LLMs will get so good at connecting the dots to the point we will no longer require traditional type-safety. We might be able to fully rely on LLMs for auto-completion without LSPs. Or maybe there will be a new protocol devised for new or dynamic-typed languages to play well with LLMs, akin to how MCPs were recently created to standardize how applications provide context and tools to LLMs, thereby bridging the gap between data sources and AI.

Alas, I have neither the skill nor foresight to come up with such a protocol. For now, I can only code, hope and pray that one day LLMs will begin to favour poetic code.