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

@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.