RubyConf 2021

Cultivating Developer-Centric DSLs

Cultivating Developer-Centric DSLs

by Jake Anderson

In the talk "Cultivating Developer-Centric DSLs" presented at RubyConf 2021, Jake Anderson of Weedmaps explores the concept of Domain Specific Languages (DSLs) to enhance development workflows and reduce the burden of writing boilerplate code. The presentation covers the motivations behind creating DSLs, particularly in repetitive workflows, and provides a framework for building effective developer-centric tools. Anderson begins by explaining the definition of a DSL as a language optimized for a specific problem rather than a general-purpose language.

Key Points Discussed:
- Understanding DSLs: The talk establishes what a DSL is, comparing it to libraries in programming. Libraries encapsulate reusable behaviors and can be seen as DSLs tailored to common tasks in programming.
- Rate Limiter Implementation: Anderson introduces a practical example of a rate limiting mechanism and demonstrates it in action. The rate limiter ensures that certain operations (like API requests) do not exceed defined thresholds.
- Constructing a DSL on Top of Rate Limiter: One of the main focuses is on how to build a DSL on top of the created rate limiter to manage API calls more intuitively. The idea is to provide an easy and consistent way for developers to implement rate limits in their applications without worrying about the underlying complexity.
- Global vs. Local Rate Limits: The discussion includes the differences between global and local rate limiting strategies, and how they can be implemented within a DSL. Anderson highlights the potential pitfalls of nesting different limiting strategies and the benefits of creating a clean interface for developers.
- Developer-Centric Design Principles: The talk emphasizes principles for effective DSL design, such as:
- Only building a DSL when necessary.
- Focusing first on developer usage before functionality.
- Ensuring consistency in usage across the project.
- Maintaining a clear and focused scope for the DSL's purpose.
- Simplifying error handling associated with the DSL.

The presentation concludes with a call to action for developers to think critically about their use of DSLs, encouraging them to prioritize clarity and usability in their designs. Key takeaways include the importance of understanding developer needs when constructing tools, as well as ensuring that added complexity does not lead to confusion or misuse, ultimately fostering a better development experience.

00:00:11.920 We've all been there where a product team might approach us and say, 'Hey, we want you to build this thing.' They’re quite anxious because they think it’s going to be really complicated. It turns out, however, that you've built some tools around that already, and it’s not as hard as they think it is. But we've also been on the flip side, where they think something is going to be really simple and easy to build, yet you might not have great tooling in place, or the tooling you have is, let’s just say, less than ideal. As a result, it gets more difficult. This talk aims to center on getting us to a point where we can efficiently handle these scenarios.
00:00:36.000 My name is Jake Anderson, and I am a Senior Software Engineer at Weedmaps, currently on the live menu team. Our responsibilities include menu management, menu curation, integration with external APIs, and pull-based crawler integrations. Weedmaps is indeed a great place to work; this is my first time at RubyConf, and I am super excited to be here.
00:00:58.399 For those who are unfamiliar with Weedmaps, we are the leading technology company and software infrastructure provider for the cannabis industry—anything and everything related to cannabis. We are actually hosting a party tonight for the entire conference, so be sure to RSVP. The party runs from 7 to 10 p.m. at the Great Divide, specifically at the Bottling Hall off of Brighton Boulevard. Just to clarify, there are two Great Divides, and the Bottling Hall is the correct location. It will be an open bar, and dinner should be a lot of fun! If you can’t scan the QR code, you can also RSVP by visiting RubyConf.org and scroll to the bottom of the Tuesday schedule.
00:01:35.920 We are also hiring at Weedmaps, so if you're interested, definitely come hit up our booth. There's a code challenge, and if you complete it successfully, you will move to the final phase of our interviews. Now, let's get to the talk. Today, I will cover a few things, starting with what a Domain Specific Language (DSL) is. Some of you might recognize the acronym DSL but may not know what it means. So we will level set a little bit there.
00:02:19.840 We'll dive right into building our own DSL based on a rate limiter. We're currently using a version of this DSL in production at Weedmaps, and it’s serving us quite well—at least, I like to think so. Then we'll discuss what we did, why we did it, how it worked, and how this knowledge can be applied to your future projects. So, what is a DSL? A Domain Specific Language is defined by Martin Fowler as a computer language tailored to solve a particular kind of problem, as opposed to a general-purpose language aimed at a broader software problem.
00:03:38.640 Initially, I didn’t fully grasp this concept, but as soon as I started thinking about it in terms of libraries, I realized that libraries can be considered DSLS. They take Ruby, and if they discover a behavior they want to repeat, they create a library around it, essentially forming a DSL to help you use Ruby to accomplish a specific behavior or task. Other concerns or modules could also be seen as DSLs. Ultimately, any reusable behavior throughout your code, especially if you're using something more than once, suggests a good opportunity for establishing a DSL.
00:04:05.200 Now that we have an idea of what DSLS are at a high level, let’s dive right into some code and build something that makes sense, followed by a discussion about it. So, what is a rate limiter? For those who aren’t familiar, a rate limiter restricts the execution frequency of a piece of code over time. For instance, if you only want an operation to occur 100 times per minute or 10 times per second, a rate limiter is a way to accomplish that.
00:04:29.360 Common examples include API rate limiters, like how Stripe only allows you to hit their endpoints so often, necessitating a rate limiter on your end to avoid exceeding those limits and receiving 429 errors. Other examples might be login attempts—where you'd want to prevent brute force attacks by limiting login tries to 10 per minute—or controls for coupon code redemptions or text messages. For instance, if a background job were to run 1,000 times on accident, sending a thousand text messages to one person would necessitate a rate limiter to prevent sending more than one message per hour.
00:05:31.520 In this demo, I'll illustrate a rate limiter in action. I have a simple method called `puts.dot`, which prints a dot onto the screen. On the left side, it will print a dot every half second. On the right side, it will try to print as many dots as possible but will share the same rate limit of five dots per second. Pay attention to the cadence on the left and notice how the right side behaves as it reaches the limit.
00:06:08.720 As you observe, the left side maintains a sequential flow while the right side, upon reaching the limit, must pause. After the right side finishes its requests, the left side resumes its regular pacing. Let’s run the demo one more time. Notice how sequentially the left side executes while the right side takes up bandwidth, illustrating the effects of the rate limit.
00:06:36.560 Regarding the rate limiter class, I won’t dive deep into its specifics, but I will provide a gist at the end if you're interested in how the rate limiter is implemented. The focus here is on building a DSL atop this rate limiter. The primary function of this rate limiter class is to accept a first argument, which is the bucket name, serving as the key to look up the rate limit count—such as how many times an action has occurred.
00:07:16.800 In this instance, the rate limiter is supported by Redis, allowing both the process we saw earlier to run while respecting the same rate limit. The `within_limit` method is crucial; it accepts a block and will execute that block immediately if it’s within the limit. If it isn’t, it will enqueue a sleep, wait for the limit to be available, and then try again. This way, it puts the operation on hold until the limit resets.
00:08:43.200 We can use this rate limiter as follows: we define it at the bottom of our class to avoid instantiating it multiple times. In each method we create, such as `thing_one` and `thing_two`, we call our rate limiter and use the `within_limit` method to execute our logic. Regardless of whether you call `thing_one` or `thing_two`, you will always respect the same API limits across your application.
00:09:11.200 However, as demonstrated, the right side is consuming the entire bandwidth. If your limits are set to 100 per minute and one class utilizes all of that, nothing else can function—this isn't ideal. So, can we create a solution to prevent one class from entirely monopolizing the rate limit? This leads us to the concept of local versus global rate limits.
00:09:38.400 Imagine we have a function, `thing_one`, where we try to obtain the local rate limit first. If it exists, we obtain that and then try to get the global rate limit before executing our block. This approach can become cumbersome due to the nesting, leading to some convoluted code that may not be easy to maintain. One way to streamline this could be to wrap these behaviors into a DSL to simplify its usage.
00:10:08.640 When thinking about writing a more developer-centric DSL, it’s critical to focus on how it will be used first before considering the implementation. Additionally, I recommend avoiding the first pattern you think of, as it may not be the optimal one. If you collaborate with your team, gather their opinions and thoughts on the DSL’s design.
00:10:50.560 Let’s present two different options for constructing the DSL. One approach would be to create a method within a class called `within_rate_limit`, which takes a block and manages the global and local rate limits for you. This is excellent when you consistently need this function to be rate-limited—say, when interacting with an external API—ensuring it’s always executed with the correct limits. Conversely, if you have functions like `thing_two`, which sometimes need to be rate-limited but sometimes not, we could employ metaprogramming to design a method that prefixes it with `rate_limited`.
00:11:45.920 But we need to consider how these rate limits will be configured. One effective way is through module and class-level configurations. If you're familiar with Rails, this is similar to how Devise handles authentication—where you include a module into the Ruby class to define configuration options. We will mimic that functionality to allow for defining local and global rate limits.
00:12:32.240 For example, suppose we define our rate limiter with a unique bucket name, allowing us to specify our global and local rate limits. In our example, we might configure the global limit to allow 100 requests per minute, while still permitting each individual service to call it only once per minute. This approach offers clarity—when developers review a Ruby class, they can quickly identify the rate-limiting configuration.
00:13:28.320 Next, let's get coding. We’ll start by establishing the module and the foundational configuration. While some may come from the Rails world, we can achieve this with pure Ruby. The included method on a class allows you to extend class methods to any class the module is included in. This is how we take our rate limiter definition and establish it at the top of the class.
00:14:02.880 Now we have the methods in place, extending the class with the ability to set up rate limits. However, nothing is implemented yet—this merely prepares the structure. If you were to deploy this to production, it would pass through without any rate-limiting effect. Clearly, we need to integrate some functionality to our DSL.
00:14:35.360 Let’s enhance our structure to incorporate functional elements. We're going to implement logic to retrieve the local and global limits, passing the respective block through. This helps create a simpler interface; there’s no need to worry about the order in which local or global limits are retrieved—by encapsulating it in our `within_rate_limit` method, we alleviate that complication.
00:15:18.640 Additionally, I will illustrate a second option using metaprogramming. This involves utilizing Ruby's `method_missing` functionality. If you attempt to call a method called `thing_three` that doesn’t exist, `method_missing` will execute and provide the name of the method as well as any arguments. This allows you to implement whatever behaviors are necessary on the fly.
00:16:13.760 We check whether the invoked method has the `rate_limited` prefix. If it does, we can wrap it within our `within_rate_limit` block. This way, when calling `rate_limited_thing_two`, we can remove the prefix and pass the arguments to call the original method directly and efficiently.
00:17:03.760 However, it's generally unwise to provide multiple methods for the same functionality. If two alternatives exist for implementing a rate limiter, teams will likely adopt different practices, complicating any future efforts to introduce changes. In this case, we demonstrated both approaches merely to display different implementations of a DSL.
00:17:45.600 Now, let’s explore customization further. Suppose we have a service that only needs a global limit, meaning it can utilize all bandwidth without restrictions. Conversely, there could be a situation whereby local limits are not required, or one forgets to include rate limit definitions altogether—when this occurs, we can allow everything through without invoking the rate limiter.
00:18:36.720 By revisiting our rate limiter class, currently through the `within_limit` method, we may call the block immediately if it’s deemed unlimited. Should a bucket name or a rate not be specified, we default to pass everything through without restriction. For example, by calling `nil.to_i`, any undefined or nil bucket will default to zero, treating the rate limit as unlimited.
00:19:23.600 Thus, we establish a conditional rate limiter that accommodates whether to include a global or local limit. If neither is provided, it merely passes through as if no rate limits were implemented. When both are specified, it respects those definitions, ensuring the functionality of the service runs smoothly.
00:20:08.800 Here’s the culmination of our presentation—with a QR code to access the gist containing the rate limitable module for your reference. You can also find the rate limiter code there. All the additions and implementations discussed are compiled for easy reference.
00:20:56.240 By engaging in this DSL construction, we've gleaned essential insights about developer-centric design. Five principles stand out as guiding philosophies for creating DSLS that align closely with developer needs.
00:21:10.560 First and foremost, only build a DSL if it’s truly needed. While creating engaging and interesting tools in Ruby can be fun, don’t proceed unless the circumstances warrant it. In our instance, we had complexities arising from a lack of clarity and reliability, giving reason enough to implement a DSL.
00:21:58.400 Secondly, always prioritize how developers will utilize the DSL before delving into functionality. We established key features, such as the `within_rate_limit` method and two different metaprogramming functionalities, after first considering how we want developers to interact with the rate limiter.
00:22:38.600 Moreover, remember to explore multiple design patterns, as the initial idea may not necessarily be the best solution. While working on this talk, I devised meta programming concepts that might have lent themselves better overall, further emphasizing the importance of collaboration.
00:23:15.280 Favor consistency in your DSLs. When working within a DSL, developers appreciate the predictability of interface behaviors. When they recognize a `rate_limitable` module in a Ruby class, they immediately know how to apply rate limiting without confusion.
00:23:58.400 Focus on keeping your DSLs specialized and targeted. In our case, we kept the rate limiting functionality broad enough to apply to various use cases, like APIs or text messages, without expanding the feature set excessively. Over time, it’s often easier to add new functionalities than it is to strip out those you realize are unnecessary.
00:24:34.320 Lastly, prioritize making error tracking straightforward and intuitive during the implementation process. Symptoms of failed executions through meta-programming can lead to confusion among developers. Custom errors in our `method_missing` implementation could point more directly to the issues arising from the DSL.
00:25:11.400 As I conclude this talk, consider ways to enhance your rate limiter or develop new functionalities around the described concepts. I hope you gained insight into creating developer-centric DSLs and how to approach building them making them valuable within your codebase.
00:25:47.679 If you have any further questions, feel free to reach out to me at the Weedmaps booth. Thank you for your attention!