When your code speaks Rails instead of the domain
I had a different plan for this section: four articles explaining the foundations of DDD compared to a typical Rails way approach. Then I asked myself: can AI also write this? And since the answer was yes for about 75% of the content, I decided to delete all four articles and start over.
All that matters now, when you write about software engineering, is the very own stuff that AI can’t produce: honest opinions, battle scars, bitter production lessons, bold statements, and the taste for picking or recommending what’s right based on the complex context.
A world without the DDD has a recurring problem: the telephone game, software-engineering edition. I worked on an HR project; every two or three months, I had a call with the clients, people actually using the platform. One conversation about our Workday integration went like this:
- Client: on the hired referrals report, I see people who were not hired, and we mistakenly paid a referral bonus
- Me: I checked the logic, and we only show records with status
hiredfrom the Workday API - Client: a hired person is someone who actually came to the office and has a
start_dateset. Some candidates never show up at all
A clear gap in the communication. The kind of gap that leads to a financial loss, a missed deadline, or a scope that quietly misses the point. Nobody in the chain ever examined the word “hired”. Each person assumed they knew what it meant and passed it along.
A customer success specialist just received a request from a client to build a feature that displays hired candidates from the Workday system. A new Jira ticket has landed: build a report of hired candidates from Workday. The developer implemented it after reviewing the API documentation.
This is precisely what a shared language, agreed before any code, would have caught on day one. The number-one goal of ubiquitous language in DDD is to remove ambiguity.
Generalization that costs
Let’s stay a little longer in the HR context and the Workday integration, as it clearly shows how the lack of proper DDD hurts Rails projects. We used to operate on the Job model and thought that the words Requisition and Position were just synonyms for it. It was a mistake.
Within the Workday integration, the position was a seat in the organizational structure that existed whether or not it was filled. A requisition was the authorization to hire, and it could be evergreen or multi-position. But in the system, the fat Job model represented both, with tens of columns to handle all possible business cases. Just to mention a few:
integration_x_publishedintegration_z_posted_dateintegration_data- a container for additional data from various integrations, with pieces probably never used
The other models followed the same approach. A popular approach to making models slim is to extract logic into service objects. We also had plenty of them. There was a separation of integrations, but it was just a false impression, as the typical service looked like that:
module Integration
module Services
class JobCreationService
def call
return existing_job if existing_job
job = Job.new(job_attributes)
job.save!
job.perform_some_other_action
job
end
private
def existing_job
# query DB
end
def job_attributes
# prepare attributes by calling API and various parsing methods
{
some_key: some_value
}
end
def some_value
# parse API response
end
def some_raw_value
# call API
end
end
end
end
You can see the violation of many rules above. First of all, the service does not just create a job. It calls the database, calls the API, parses the response, and parses other attributes, and you don’t know the radius of changes when you hit Job#save because of multiple callbacks on the model.
A typical service object is created solely to satisfy the requirements of a Jira ticket for a given task. No anti-corruption layer (a translation boundary that stops Workday's model from leaking into ours), crossing multiple boundaries, and tight coupling between the external source of data and the internal state. This is the kind of mess that produces bugs like the bonus we paid.
Naming the disease
None of this would have happened if we had examined the language first. We didn't, so we built a model that generalizes concepts the business keeps distinct. We treated Requisition and Position as synonyms for Job. We treated a Workday hired status as a hired person. In both cases, the code ended up speaking in terms of Rails rather than the domain. That's the disease. DDD is the cure.
It starts with a shared language among everyone involved: the client, the customer success specialist, and the developer all meaning the same thing by the same word. The two stories show the two sides of DDD:
- Strategic - we would have caught it on the call: that a "hired" candidate has a
start_date, and that a Position and a Requisition are different things, not two names for one. - Tactical - the code would have reflected those distinctions: hired modeled the way the business means it, so the report couldn't return no-shows, and Position and Requisition as separate concepts instead of one fat
Job.
But a shared language is not enough on its own. Before you can protect it in code, you have to find where one language ends, and another begins - and that means discovering subdomains and drawing the bounded contexts around them. That's where the next article begins.