Talks

Measure Twice, Cut Once

Measure Twice, Cut Once

by Alec Clarke

The video "Measure Twice, Cut Once" by Alec Clarke explores the parallels between woodworking and software development, illustrating how lessons from one discipline can enhance skills in the other. Alec, a senior software developer and woodworking enthusiast, shares his journey of learning to build a shaker table and the insights he gained, which can also be applicable to coding practices.

Key Points Discussed:

- Safety First: Just like safety precautions in woodworking are crucial to prevent accidents, preventative measures in coding, such as careful error handling and rollback plans, are essential for avoiding critical issues in software development. Alec emphasizes implementing safety mechanisms to mitigate risks when deploying new code.

  • Solid Foundations: Preparation of raw materials in woodworking mirrors the need for a stable codebase. Alec underscores the importance of refining code structure before implementing new changes. He uses the example of changing a material purchase method to show how preparing the code for new implementations can prevent bugs and errors.

  • Quality Control: The speaker explains the process of creating jigs for consistent and high-quality cuts in woodworking, paralleling this with the need for templates and generators in coding. He suggests using Rails' features to streamline the creation of maintenance jobs, ensuring quality while reducing overhead for developers.

Significant Examples:

- Alec relates his experience with the table saw, explaining tools to prevent mishaps and how this mindset can transfer to coding practices by managing risk during development.

- The construction of the shaker table acts as a metaphor throughout the video, illustrating the methodical approach required in both woodworking and software development, reinforcing that careful planning, attention to detail, and consistent quality lead to successful outcomes.

Conclusions/Takeaways:

- Alec concludes that integrating a safety-first mindset, a solid foundational approach in code structuring, and ensuring quality control can significantly enhance a developer's impact. These strategies not only promote personal growth in woodworking but also cultivate effective coding practices that lead to better software development outcomes.

He invites viewers to consider how skills in one area can enrich and improve capabilities in another, particularly emphasizing the ongoing influence of woodworking on his coding skills.

00:00:09.269 Welcome to this couch edition of RailsConf 2020. My name's Alec, and I'm a senior software developer at Clio, where we work on transforming the practice of law for good.
00:00:14.530 Now, early on, I was introduced to woodworking. I spent many afternoons in my grandfather's workshop watching him transform rough cuts of material into beautiful pieces of furniture. That ability to create such transformations really stuck with me. So, it was no surprise that a number of years later, I also took up woodworking.
00:00:26.200 I started with beginner projects like picture frames and building a box. Eventually, after honing my skills for some time, I graduated to more advanced topics like building a rocking chair. However, as I graduated from school and moved away to start my career as a developer, I lost touch with woodworking. It wasn't until this past year that I was able to rekindle that passion when I decided to enroll in furniture building courses at my local community college.
00:01:03.989 The focal point of the first course was building a shaker table. The shaker design is relatively simple, featuring nice, clean lines and tapered cuts on each leg, along with small details that give it a classic look. While it's a simple build, the shaker table does an excellent job of introducing beginner woodworkers to the basic techniques required to create quality furniture.
00:01:51.070 Going into this course, I expected to learn a lot about woodworking, but I was surprised to find that I also learned much about writing better code and becoming a better developer. In this talk, I want to walk you through my journey of learning to build this table and share three lessons it taught me about being a more impactful developer.
00:02:17.800 No better place to start is with safety. The first lesson in the workshop is always about safety—at least it should be—if you want to keep all your fingers intact. This was no different in the course I was taking. We took time to understand how each tool worked, what its purpose was, the risks associated with using them, and how to protect ourselves from potential hazards.
00:02:30.149 For instance, this is the table saw, usually the workhorse of most shops. It's used for making straight, accurate cuts efficiently, and getting comfortable with it is crucial since it's involved in at least one step of most projects. However, it's also responsible for the majority of workshop accidents. Fortunately, many of these accidents can be prevented by using standard safety mechanisms for operating the saw. These include using a blade guard whenever possible, which is a plastic shield that sits over the blade, allowing the material being cut to slide under the blade and keeping your hands safely away.
00:03:11.870 Similar to the blade guard, there's a tool called a push stick. Though it seems primitive—and it is—it helps keep your hand at a safe distance from the blade while making narrow cuts. It also provides extra leverage to push the material all the way through without leaning over the dangerous part of the saw. These safety mechanisms can easily be built into a checklist we follow every time we use the saw. They protect us from severe accidents and ensure we have long, safe careers in the workshop.
00:04:15.520 When coding, we might not be at risk of losing a finger, but that doesn’t mean we shouldn't take steps to prevent critical issues from arising and ensure we can recover quickly if they do occur. To illustrate what that looks like, let's introduce a contrived Rails app example for this talk. We'll consider an online storefront for our artisan table furniture store, where customers can place orders for finely crafted handmade furniture.
00:04:44.780 In our app, we have a MaterialPurchaser class. As orders are placed, we need to purchase the raw materials needed to construct them. The purchase_material method accepts an order and a list of project materials as its arguments, iterating through the materials and sending a request to the lumber supplier to ensure we acquire what we need. Once that's completed, we mark the order as having had its material purchased.
00:05:07.390 Lately, we faced a requirement change; the lumber we're receiving back from our supplier is satisfactory, but we need to switch to the better lumber option. Fortunately, both suppliers have consistent APIs and that was the only change required. However, after we made the change, our error monitoring detected some exceptions, and we quickly realized a bug was introduced; new orders were failing to mark their materials as purchased. We initiated a revert, got sign-off on it, and then had to wait for our painfully slow CI process to finish before we could merge it and deploy.
00:06:03.550 That's a pretty painful process for getting a quick fix back into production. If we had adopted a safety-first mindset upfront, things could have been much smoother. Ideally, we could wrap our change with a conditional that said, 'As long as everything is functioning normally, use this new code; otherwise, default back to the previous trusted code.' Furthermore, we could enhance that condition by allowing only a subset of accounts to access the new code, incrementally rolling it out to the remaining accounts over time.
00:06:41.500 This method would let us test the waters before exposing our entire user base to a bug. We would also want it to be consistent so that if a customer was using the new code path, they could remain on it without interruption, avoiding any flip-flopping between the two.
00:07:46.370 Most importantly, though, we would want the ability to roll back instantly, without having to go through the deploy process again. Now, let’s build that. We will start by creating a new resource called the StageRolloutState that will manage our incremental rollout logic.
00:08:05.790 As a starting point, we’ll look at the migration in the staged rollouts table. We will define two key columns: first, a name column to provide contextual information about the rollout, which enables us to reference it in many places, not just for a single change. The second column will be a started_at timestamp indicating when the rollout began.
00:08:45.260 Next, we'll implement a 'ramp_up_time' of eight hours, representing how long it takes to transition user traffic from zero percent to one hundred percent. We'll also create a method to calculate 'percent_enabled', which will return the percentage of users actively experiencing that rollout.
00:09:01.360 Once the rollout has started, we need a better interface to interact with beyond just calling the model itself. Therefore, we’ll develop a RolloutService. This service will have an 'enabled_for' method, which, given the name of a rollout and an account ID, will return a boolean indicating whether that account has access to the new code.
00:09:55.550 We want this response to be consistent, so once it is true, it will always remain true. To achieve this, we need to create or find the rollout based on the provided name and, utilizing some modular arithmetic, define a formula that provides that consistent response. I won't dive into the math involved, but I encourage you to pause and review it after this talk.
00:10:37.060 This strategy has the significant benefit of not relying on any sort of persistence to maintain the state of whether an account has access to a given staged rollout. Finally, we need to ensure we can roll back instantly. We can connect our Stage Rollout resource to the admin panel and provide a simple user interface that allows us to start, stop, or pause the rollout without changing any code.
00:11:03.180 Returning to our hypothetical goal, we stated earlier that to accomplish this we need to modify the condition to call the rollout service's 'enabled_for' method. Once that's done, when we ship that change, we are going to hit the same bug we experienced before, but it will only affect a subset of accounts. Consequently, getting back to a stable state will simply be one click away.
00:11:55.250 Now, the memorable aspect here is the orders that were missing material for construction. Let's fix that by jumping into the production Rails console and performing our magic—we can find all the orders that are lacking material and proceed to repurchase it. However, there’s an important caveat: this approach isn’t testable, and undoing the changes once you've executed them in the console is nearly impossible.
00:12:39.710 This method lacks proper review unless someone is screen-sharing or standing over your shoulder, which isn't ideal. More importantly, to run this approach, developers would need access to the production environment, exposing them to sensitive customer data.
00:13:02.640 If we can’t perform this safely, what’s the alternative for quickly fixing issues? We know we need a testable fix, and it should be a one-off solution since this isn’t something that will be executed numerous times. Each fix is unique and should be hands-off, requiring no access to production.
00:13:38.860 A suitable starting point might be creating a new application job that will run our fix. We'll take the same fix we wanted to execute in the production console and move it into an application job. This setup will be more seamlessly testable.
00:14:13.700 However, the application job lacks the one-off functionality we're aiming for. To enhance this, let’s inherit from a new type of job class called a MaintenanceJob. In this MaintenanceJob, we'll define a version attribute that allows subclasses the flexibility to specify their unique version, which will provide a timestamp and enable running the job only once.
00:14:57.709 We'll create a MaintenanceRun record to store that version, ensuring it is persisted. Before queuing any new maintenance jobs, we will record that run, employing a runnable check that flags the maintenance run as no longer runnable for a given version once it is recorded.
00:15:48.720 Now we accomplish the one-off functionalities we’re targeting, but we still have two additional steps to make it fully hands-off. First, we define a 'run_pending_jobs' method for the maintenance job, which finds all class descendants yet to be run and will queue them for later execution. Then, we integrate this step into our deploy script to trigger the 'run_pending_jobs' method whenever we ship a new maintenance job fix, ensuring it is automatically queued and executed.
00:16:38.390 Now returning to the fix for the orders missing materials, we’ve inherited from the MaintenanceJob. All we need to do is define a new version, and the code to fix that issue is implemented automatically.
00:17:10.640 By adopting a safety-first mindset and integrating safety mechanisms, we significantly reduce the time spent scrambling to fix problems later. This proactive approach also protects our customers from delays in fixes.
00:17:38.020 Next, we need to establish solid foundations. After learning how to safely conduct ourselves in the workshop, it was time to begin constructing the table itself. We started with rough material, for instance, if we went straight to cutting pieces of the table from this unprepared wood, we would likely end up with a compromised result.
00:18:06.760 In woodworking, a vital step is the lumber prep process, which aims to eradicate various types of deformations that naturally occur in wood, like bows or twists. We need to eliminate these defects to create a solid foundation for building, and two machines are particularly useful in this process—the jointer and the thickness planer.
00:18:59.950 The jointer consists of a long flatbed with a rotating cutter head in the middle. Each time lumber passes over it, a small amount of material is removed. If a board has high points, the jointer gradually removes those until the lumber is perfectly flat. The goal is to get one flat face and one straight edge. After that, we move to the thickness planer, which works in the opposite way.
00:19:56.330 The thickness planer removes a small slice from the top surface of the lumber, using the already flat bottom to ensure that the top remains parallel. This process allows us to achieve the exact thickness we need for the material. After prepping this rough material, we ended up with a solid, flat foundation to begin cutting out the table legs.
00:20:54.379 Avoiding the lumber prep process would hinder our ability to create straight and consistent table legs. Similarly, in coding, we often start off in a rough state; it's uncommon to begin from a pristine position, so we should take the time to prepare our code before implementing further changes.
00:21:46.860 Looking back at the MaterialPurchaser class we discussed earlier, even though we implemented a rollout, we still ran into bugs. Thus, we need to examine that method to see why. Digging a little deeper reveals that the method was in a rather unrefined state. The issue originated from human error; we used the wrong index to access the type value in the material array provided by our better lumber supplier.
00:22:29.030 Ideally, we should have taken the time to prepare the MaterialPurchaser class thoroughly before adding this new lumber provider and rollout. Returning to the original code state, we identify the necessity of addressing the most pressing issues first, such as clarifying the structure for the material array.
00:23:22.659 We can introduce a new class dedicated to representing the material, holding details like thickness, width, length, and type of material being purchased. Then, in the purchase_material method, we can map incoming materials to an instance of this new class, allowing updates to be handled semantically rather than relying on the index structure.
00:24:06.060 Using ActiveSupport magic, we can then call 'to_json' on the material instance itself, thus sparing us from needing to explicitly define each parameter. We should also move the supplier URI definition outside of the loop since it only needs to be defined once, which transforms our code into a much more stable state.
00:24:59.340 With a flat surface prepared, we will find it much easier to roll out the new lumber provider. In our rollback, we would only need to wrap the URI without altering anything else, maintaining consistency during this change.
00:25:54.860 Taking the time to establish a solid foundation in our code can yield numerous benefits, not only providing a clearer understanding of its function but also ensuring a consistent structure for future developments.
00:26:34.716 After cutting out the table legs, we knew that the shaker design required tapered cuts on those legs. There are numerous ways to create these tapers. For instance, one method involves marking the taper on each leg with a pencil and then using the bandsaw for a rough cut.
00:27:00.690 While this could work, it wouldn’t be efficient for multiple projects because each table requires eight tapers, and with twenty students in the class, it would take an unconsidered amount of time. Instead, we could create a jig holding each table leg at a consistent angle for the tapering process.
00:27:18.370 Using this approach allows us to cut the exact taper we need in one pass with the saw, resulting in quality cuts and reducing the overhead associated with maintaining that quality. Similar to the tapers we cut on the legs, in our projects and teams, we often encounter reusable patterns.
00:28:26.010 However, we must ensure that the quality of our work remains consistent. Referring back to the MaintenanceJob we created, it serves as a useful structure, albeit we must still take steps to make it reusable.
00:28:55.300 If we wanted to create a new maintenance job, we might have to revisit previous examples, manually copy-pasting to see if it’s applicable to our new fixes. Furthermore, we might not recognize the significance of the version numbers, leading to challenges in executing those jobs properly.
00:29:47.040 Fortunately, Rails provides a solution with the built-in Rails generator. This allows us to construct a maintenance job generator effortlessly, giving us a boilerplate to create a new generator for each maintenance task.
00:30:29.500 To flesh this out, we’ll establish a template for the maintenance job and streamline the process that currently confounds version tracking, taking this burden away from the developer so they can focus on their work.
00:31:12.600 We'll simply generate the current timestamp in a specific format to serve as the version. Furthermore, we'll incorporate a template for creating a test for the MaintenanceJob, as we hope every developer implements a test before shipping fixes.
00:31:50.010 However, to ensure our tests remain effective, we must set up the test to intentionally fail by default, prompting developers to return and address it. This enables the Maintenance Job generator to create a maintenance job file based on the templates with efficient quality control built-in.
00:32:32.960 When everything is in place, we’ll enjoy consistent quality where the version is predefined, and we ensure that every fix undergoes proper testing. Maintaining quality remains a critical step for any project we undertake, but minimizing the overhead of managing that quality is equally necessary.
00:33:08.720 With reduced overhead, we can focus on tackling the new challenges that lie ahead. As we conclude my talk today on the three lessons I've learned from building that table, it’s essential to recognize that this journey didn’t halt here.
00:33:54.820 After crafting those table legs and ensuring the tapers matched, the remaining pieces began to fall into place. Over time, the table started to take form, eventually leading to the finished product.
00:34:36.670 I could not have reached this final state without my safety-first mindset guiding me through, ensuring a solid foundation for each piece I worked on. On this continuing woodworking journey, I anticipate that it will profoundly influence my coding practices. Thank you for your time.