00:00:04.860
Hello, I'm Anthony Eden, the founder and CEO of DNSimple, a domain management automation platform for all your domain name and DNS needs. Today, I'd like to talk about what it's like running a Rails application for more than ten years.
00:00:10.860
First, I'm going to go through a brief history of DNSimple and the application itself. Next, I'll discuss the software design choices I made early on and how they have evolved over time as our team and the application have grown.
00:00:22.920
Next, I'm going to talk about the evolution of testing at DNSimple, followed by how we operate DNSimple. This has had a dramatic impact over time on both the design of the application and the system as a whole. Then, I'll cover a bit about dependency management – how we handled it early, how we approached it later, and how we manage it now.
00:00:39.300
I will also discuss some changes in our team over time and how these have impacted the design of the application and its ongoing operations. Finally, I’ll touch on the future and some of the challenges we still face today.
00:00:59.100
Let's start with a little bit of history about DNSimple. The DNSimple application is a Rails application that was first created on April 7, 2010, which marked the initial commit. In the beginning, there was only one developer working on the Rails application – that was me. The other person who helped create the business was focused entirely on the operations side, particularly on the DNS infrastructure.
00:01:22.560
From the initial commit to the launch, it took about three months to create a minimum viable product that included only DNS services. Throughout those first three months, I made several decisions based on the best practices of Rails at the time, and I am sure I made quite a few mistakes along the way – which is to be expected.
00:01:54.899
Let's take a look at some of those early software design choices and how they have changed over time. During those early days, I was following the Rails best practices that were widely accepted at the time.
00:02:12.780
The first example I'd like to discuss is the use of the 'thin controller, fat model' principle. While the thin controller concept is an excellent idea and still holds true today, the fat model approach, while manageable for smaller applications, becomes more difficult to maintain in larger applications.
00:02:48.620
Here's an example of the register method from one of the earliest versions of the domain model. This was used to register a domain name by taking the necessary extended attributes for the domain, creating an options hash, passing that to the purchase method in the upstream provider model, and then checking if the returned result included a valid order ID. If it did, it would set the registration status to 'registered,' save the local model, and return the result of that save; otherwise, it would return false and log the relevant errors.
00:03:38.760
Now, let's take a look at the controller corresponding to this action. If the register operation was successful, the application would deliver an email notifying the customer that the domain was registered. However, I wasn’t entirely adhering to the thin model concept, as complex business logic was already slipping into multiple places in the early versions of the DNSimple application.
00:04:41.460
Additionally, I was utilizing ActiveRecord callbacks fairly extensively. For instance, before creating a subscription in Chargebee, a callback would attempt to create it there and, if successful, set the Chargebee subscription ID; the opposite would occur for destruction. Meanwhile, the direct access to the ActiveRecord query interface was scattered throughout various portions of the code, which complicated understanding and maintaining query logic.
00:05:39.300
As a consequence, we faced several challenges with this approach. The use of God objects emerged, which made the models complex and large with many responsibilities. This led to difficulties in understanding and evolving the system due to the tightly-coupled methods and behaviors within these God objects.
00:06:47.460
In particular, the use of ActiveRecord callbacks further complicated our design. They often reached outside the model's intended area of responsibility, causing tight coupling with other system components, which resulted in a lack of clarity and ease of evolving the application.
00:07:53.640
To address some of these issues, we introduced the concept of commands. In traditional patterns, commands encapsulate an action and are designed to facilitate actions that can be undone and redone. Initially, I aimed to create commands in a way that would allow straightforward execution behaviors for various tasks, but over time, they became a means to aggregate business logic and provide integration across multiple components.
00:08:54.420
To illustrate, here is how the contact create command would be called today. The command execution takes a context as the first argument and accepts additional parameters afterward, which might hold information about the current account and user context. Instead of tightly coupling logic in the controller, the command encapsulates the operations, which enhances clarity and maintainability.
00:09:45.600
As we transitioned, we saw significant changes. For example, the command instance method now handles parameters more cleanly. The command implementation creates the contact directly, and upon success, sends notifications without cluttering the controller logic.
00:10:55.200
Another important addition to our architecture was the concept of finders. This is loosely based on the repository pattern, allowing us to have a single place to look up collections or instances of specific models. This approach ensures consistent methods for retrieving contacts across the system.
00:11:36.780
The evolution of finders is evident. Initially, we only pulled from users, but as we structured our application around accounts, we refined our approach, ensuring all entities were properly linked to accounts rather than users. This encapsulation facilitates easier access and ordering of data in a more streamlined way.
00:12:44.400
Now, let’s shift gears and examine the evolution of testing at DNSimple. In the earliest versions, we leaned heavily on Cucumber and RSpec for testing purposes, primarily focusing on acceptance tests rather than unit tests.
00:13:36.379
Here's an example of an early acceptance test demonstrating that a user can activate their account. It includes a description of the desired feature and outlines the scenarios to activate the account through the user interface.
00:14:04.920
However, as our system grew, we determined that maintaining Cucumber tests became too time-consuming due to frequent interfaces and application changes. So, we transitioned to using RSpec for both unit and integration tests.
00:15:06.240
Running the entire test suite can be slow, but it allows us to execute focused unit tests, even though there's room for improvement. This challenge is common for applications that continue to expand without a dedicated focus on unit tests.
00:16:14.280
Let’s discuss the operational evolution of DNSimple. In our early days, we utilized both PostgreSQL and MySQL. While PostgreSQL served as our primary data store for various models, MySQL was used specifically for storing our DNS server zones.
00:17:32.580
As we scaled, we transitioned to an Anycast system, distributing our authoritative DNS servers across multiple global locations and shifting from virtual private servers to bare-metal servers. This transition significantly redefined how we handle data distribution and storage.
00:18:14.280
The evolution of our background job processing saw us moving from Resque to Sidekiq and eventually to Sidekiq Enterprise. This required us to change our application to abstract direct dependencies on Resque, paving the way for a more flexible architecture.
00:19:59.820
In terms of our authoritative DNS, we adapted from a simpler operational model dependent on a few VPS instances, moving toward more robust implementations that enhance durability and performance.
00:20:54.900
Next, let's talk about dependency management. Any long-standing application with external dependencies must approach dependency management thoughtfully, and initially, we relied heavily on an ad hoc method.
00:21:44.100
Updating dependencies was a manual process without any structured methodology, leading to challenges when transitioning between different Ruby and Rails versions throughout our journey.
00:22:45.600
As we matured as a team, we began adopting a manual yet well-defined policy surrounding dependency updates, eventually transitioning into an automated approach with Dependabot to handle updates seamlessly.
00:23:55.560
Moving on to team changes, the impact of new hires on the application's evolution has been significant. The initial version was the sole effort of one developer, yet bringing in new talent sparked fresh ideas and suggestions for improvement.
00:24:56.760
As the team expands and contracts, we face the challenges of ensuring cohesion and consistency across the code. Each developer brings unique perspectives that contribute to evolving our application standards.
00:25:57.060
With varying levels of expertise among team members, we constantly provide opportunities for information sharing and knowledge transfer through pairing or even mob programming, enhancing our collaborative efforts.
00:26:56.700
While strong opinions and coding styles can spark diversity in thought, they can also create friction within the team. Navigating these complexities is important as we aim to maintain harmony within the growing codebase.
00:27:42.360
An additional challenge we continually address is consistency, particularly in documentation. Our early codebase lacked clear documentation, but over the years, we have increased our efforts to establish better documentation practices.
00:28:34.500
This has included developing a team Wiki for maintaining guides and ensuring every area of our application is well-structured and easily navigable. With over 27,000 commits and ongoing development, we strive to keep our system manageable.
00:29:38.520
As we look to the future, we continue to face challenges like managing legacy choices and increasing complexity. Each decision made today will impact the application's design and maintainability moving forward.
00:30:32.020
Managing complexity is a priority as we aim to break our systems into more discrete subsystems, which will remain cohesive within our Rails framework while fostering independent maintenance capabilities.
00:31:50.940
In closing, maintaining a long-term Rails application is a challenge, but it is also immensely rewarding. We benefit from the strong foundation of the Rails community and anticipate many more years of successful application development.