Happy New Year! It's 2023 🎉 Joël and Stephanie chat about developer resolutions or things they'd like to do this year and then discuss componentization and branching strategies.
This episode is brought to you by Airbrake. Visit Frictionless error monitoring and performance insight for your app stack.
- Joël's Elm Meetup talk wasn't recorded but the slides are here
- Joël hasn't published an article on the 3 principles of branching, but he does discuss them in this twitter thread
- Making Impossible States Impossible
- Component-Driven Development
- View Components at GitHub
- Confident Ruby
- Case Expressions Episode
JOËL: Hello and welcome to another episode of The Bike Shed, a weekly podcast from your friends at thoughtbot about developing great software. I'm Joël Quenneville.
STEPHANIE: And I'm Stephanie Minn. And together, we're here to share a bit of what we've learned along the way.
JOËL: So, as of the time that this episode goes live, it's the new year; it's 2023. Happy New Year.
STEPHANIE: Happy New Year, Joël, and Happy New Year to our listeners.
JOËL: So, new year is oftentimes where people like to take maybe resolutions or plan a little bit out of their year. I'm curious, Stephanie, do you have any developer resolutions or things that you'd like to do this year?
STEPHANIE: So, I think last episode, we were talking a little bit about reflection, especially in terms of career progression. And I may have mentioned that I don't really believe in New Year's resolutions. [laughs] But I do think that one intention that I have for myself is to chill a little bit. I think the fall of 2022, for me, was really hectic, exciting but a little busy in terms of speaking and content creation; you know, we started doing this podcast together. And so I do think that winter is my time of hibernation.
Development-wise, one goal that was really inspiring for me that you shared is writing ten blog posts a year. And I think I might keep that in the back of my head as we get into the new year and maybe start to at least just have it in my mind. So if I see anything that comes up, I can be like, oh yes, I had this intention to write more blog posts, which I think might be helpful to just marinate on even during my chill time.
Because when I do finally feel productive and energized again, I can at least have been thinking about it. And I think doing that reflection and marinating is also work, even if it doesn't end up turning into an artifact or anything like that. And so yeah, that's my plan for the beginning of the year, at least.
And then, on a personal note, I'm on a journey to be warm this winter and not be miserable and cold. So as a recovering Californian in Chicago these days, I have just always wanted to stay in and be a little bit of a hermit in the winter because I just have a hard time with the cold. But then I learned that you can just buy things that will help keep you warm.
And so, a recent purchase of mine was a heated, portable blanket. So I will definitely be repping that in my Zoom calls and on the couch. And I've just gotten really stocked up on the warm weather accessories, so fingerless gloves and the like. So I'm excited to just not be miserable this winter. That's my plan for the new year.
JOËL: You'll have to report back on how well that goes, how you feel about that blanket.
STEPHANIE: Oh yeah, I definitely will. Maybe I'll even wear it when we're recording.
JOËL: Maybe next year we can make a gift guide.
STEPHANIE: That would be so fun. I would like that: a gift guide for developers or the developer in your life.
JOËL: Right. Fingerless gloves just because it looks cool. [laughs]
STEPHANIE: I'm wearing them right now. Yeah, I can type, and my hands stay warm, highly recommend.
JOËL: So it's not just a hacker thing to look cool. It actually works.
STEPHANIE: I would say so. As someone who has very cold extremities, it's been a good solution for me.
JOËL: You'd mentioned in a previous episode talking about conference talk ideas that you like to marinate in sort of build up a file of ideas over the course of a year. And it sounds like you're taking that approach but then also applying it to smaller form content like blog posts over the course of this winter.
STEPHANIE: I think so. I realized something about myself is that I am a bit of a slow cooker in terms of coming up with ideas, especially if they're rooted in experience. I think it takes me the process of going through something like going through a client project and then honestly, three months later being like, oh, now I can have some distance from it and be like, oh, this is what I was thinking, or this is what I noticed or observed, which I think that's just how I am, and that's okay. I recognize that I don't necessarily churn out content all the time, and, for me, it kind of does feel more like seasons or cycles.
JOËL: So it's funny you mentioned the ten blog posts goal, and that's what I was going to share for my developer goal for the year. I've had that for the past several years. It's a really fun goal to have, I think, because it's aggressive enough that it requires me to put out a fair amount of content, but it's also achievable.
That being said, as of the time of this recording, which is at some undisclosed time prior to the New Year, I only have eight articles that are live on the thoughtbot blog. So for all you listeners in the future in 2023, if you're curious if I achieved my 2022 goals, go check the thoughtbot blog and see did I successfully publish the last two before the end of the month.
STEPHANIE: One thing you had mentioned to me off-air was that you do also tend to backload blog posts towards the end of the year once you realize that that deadline is coming up. Do you think you will do something differently in 2023?
JOËL: I would like to write a little bit more early in the year. I think I'll have to figure out exactly how I want some of my goals to play out. I think I mentioned this in the previous episode; two large themes that I've wanted to focus on are ways to invest in our team and our teammates and then creating content, so things like blog posts, like this podcast, like conference talks. Those were two broad themes that I had given my year in 2022. And I really enjoy those. I think I would want to repeat those themes for 2023 as well, so figuring out exactly how I'm going to interweave them, something I'm going to iterate on.
STEPHANIE: Have you considered breaking down that yearly goal into smaller time intervals, so maybe two or three blog posts a quarter?
JOËL: That might be a better way to do it to make sure that I'm on track. I give myself this as a goal. I'm not super hard on myself if I don't hit it, although I have hit it for multiple years. If I only have eight blog posts this year, that's still an accomplishment I'm proud of. And I think there's some good content that I put out. So I will not be distraught if I don't hit the 10, but it's good to be aiming for something.
STEPHANIE: Yeah, that's a good mindset to have. I also have a personal goal of reading 52 books a year. And this will be my third year attempting to do so. Or, I guess I have been successful in 2021 and 2022 now. But I remember when I first wanted to do it; I didn't tell anyone because I was terrified of not meeting that goal and just feeling a bit disappointed in myself.
And so, I just kept it to myself and didn't mention it to anyone until I got to around 40 or 45 bookmark, and then I could confidently tell people about my goal because, at that point, I was on track and feeling pretty confident that I was going to finish it. So that's my strategy [chuckles] is to not tell anyone until I am pretty much there and then share, and people can be impressed.
JOËL: I feel like there's always a bit of a tension there where when you've got a goal, sometimes you don't want to tell people about it because you don't want to say a thing and then disappoint other people and not get it done. But in some ways, for me, when I can get a goal out of my head and out into almost the real world by telling someone, it makes that goal more real and maybe inspires me to work harder towards it but also maybe helps me believe in myself a little bit more because I've said it out loud.
And, I don't know, maybe saying it in front of a mirror would have the same effect. But getting it out of just my thinking pattern and saying it, "This is what I think I'm going to achieve. This is what I'm trying to do," somehow makes the goal more real for me, makes it more achievable.
STEPHANIE: That makes sense. It's like the difference between saying, "I think I'm going to do this," and "Okay, I'm going to do this."
JOËL: Right. And maybe there's a little bit of social pressure too, if I tell someone, now I don't want to disappoint them. That can be bad because it causes me to doubt myself, but in small amounts, it can maybe help me to push through moments of doubt or moments of feeling like I want to give up.
STEPHANIE: Yeah, I mean, either way, even if you only ended up with eight blog posts in a year, people are just really excited that you're putting content out there. And they're not counting how many posts you put out.
JOËL: In a sense, it's purely a vanity metric for me to know my progress.
STEPHANIE: So I have one thing that happened this week that I would be curious to get your input on. On my client project, I was tasked with making a small UI change to a navigation menu that existed as a separate React project within our Rails repo. And so the task was for...we had this little caret icon that was used in the mobile nav, and the designer wanted it to be reused somewhere else on the desktop nav menu.
And I was digging through the codebase looking to see where the caret was already. And I realized that it was done with CSS on the menu label. So it was really coupled to this menu label and wasn't reusable in that current state. So it took me a little while to figure out how to pull out the seams and extract it into its own component so I could reuse it where I needed it to.
And so I was trying to figure out how we got here because we are using the styled-components library, which should encourage componentization. And I was just thinking about different approaches to building UI features from scratch. And I did a little bit of digging and learned about component-driven development, which suggests the idea of building each component in isolation and thinking through all the relevant states that it might exist in for at least your first couple of use cases and then combining them to create larger components, and then ultimately pages.
And that was interesting to me because it's a little bit different from a strategy that I'm used to, especially if you're implementing a new page or template where you just kind of scaffold out all of your HTML elements that you need. But then when you add on styling, those primitive components, you might end up having a lot of duplication if you are creating very generic things like buttons that end up being coupled to the page that you're working on, especially if you are putting them on the page in a very specific way.
And you might add CSS rules like margins or padding that, again, is coupled to that particular UI that you're building. So I'm curious if you've really thought about building UI from a component level and starting small and then building out or if you also take it from a top-down approach.
JOËL: It's interesting that you mentioned that the component approach really deals with figuring out state. And I think that's probably an area where it shines a lot when you have situations where components can have multiple states. And it's very easy when you're looking at much more of a scenario-driven approach where you just want to say, oh, I want this form input to look like this, like this one mock. But that was only showing the happy path. And you didn't think about all those other states. And so, for situations where you might have a lot of states, the component approach, I think, is really interesting.
I had a really fantastic experience a few years ago pairing with a thoughtbot designer on fairly...well, what we thought was going to be a simple form input. And it turned out that it had a lot of edge cases and funny state things that could happen. And we ended up drawing what essentially was a...you might call that a finite-state machine in a more formal sense. But it's basically a diagram where we show the state of the input. And then we would say, what are the different things that can happen that might transition it into another state?
So maybe it starts empty, but then you start typing, and then something happens, or you have an invalid value, and then something happens. But then, from the invalid state, can you come back to the empty state? Can you come back into the typing state? At what point do we show a read error? Do we clear it out while you're typing? Even after you have an error, this particular input was also backed by some remote data. So it was like a typeahead kind of thing pulling data from a server.
And there were a lot of extra edge cases for things like, oh, we're waiting for results, or no results matched, or we got exactly one result. And so that was really interesting. We ended up building up this whole diagram where we showed all the transitions that could happen, the ways you can loop back to a previous state, and it forced us to think about a lot of edge cases that we wouldn't have thought of otherwise.
STEPHANIE: That's really interesting. I think the transitions between different states definitely can get really complicated. While you were saying that, I was reminded of Storybook, the tool for building out components in isolation. And one thing that I really like is that they encourage you to think about different states and edge cases as almost like user stories. I think they're called stories. And you can use their DSL to extract those pieces of information and basically think through kind of what you were saying, but it's built into the tool. And so it really encourages that thought process.
Because I definitely have run into just trying to build out a basic button or something but then having all of these questions that I have to ask designers while I'm implementing it to be like, what should happen here? Or, like, what should it look like when it's disabled, or what happens when it, like you mentioned, gets back data that it wasn't expecting or something like that?
JOËL: Sometimes you have situations where your page doesn't have a lot of state, or your components don't really have a lot of state. And it really is just a static page. In those situations, now you're looking at questions more of reusability rather than state management. And you may or may not want some kind of componentized approach for that. That might be a little bit different depending on, like; you may not be using React if it's a completely static page.
So maybe this is server-rendered, and you're trying to componentize using Rails helpers. Or you're using something like BEM the CSS...I don't know if you'd call it a framework or a structural approach of defining classes that are in a more componentized approach so that you can reuse styles. So there are a lot of ways to reuse and componentize. Even though I think oftentimes when we talk about visual components, we're often thinking about React.
STEPHANIE: Yeah, I also did a little digging into ViewComponents because I was, again, just kind of trying to think of a mental model for how to approach building out UIs. And in their docs, they have a really good example about their process for using ViewComponents at GitHub. And basically, the progression is that they implement a single use case component that might live as it is for a while until there is some other use case for that component, and then maybe it's adapted for general use in multiple locations.
And then, if it turns out to be like a really good generic building block, they actually extract it into their open-source component library called Primer, I think is what it is. So that was an interesting process for me as someone who just kind of like did that first step of pulling out this little piece into its own component. And then, right now, it isn't necessarily quite ready for being reused in a bunch of different ways. But I think that was a good first step in setting it up to be able to.
JOËL: Definitely. I think it's easy to overDRY or, in this case, it's almost like over abstract in preparation for reuse that might never happen. But oftentimes, it's an incremental thing where you do as much as it makes sense for your current scenario while also leaving yourself the option to easily keep going down that path for future scenarios where there is more duplication. And then, if those scenarios never come, then great, you've saved yourself some work. And if the scenarios do come, then hopefully, it's easy to take the next step.
So if a function (But you can think component here.) is splitting in two different situations, then it doesn't get to have any logic inside it. It just calls out to some other component. And the only thing that it does is say, "I'm a branching component. If this happens, pull this other subcomponent; otherwise, bring in this subcomponent and maybe set up some arguments or something like that." And then the other child components that are rendering various pieces of UI, they don't get to branch. They are just given this data, render it in this way.
STEPHANIE: That makes a lot of sense. I think that also reminds me of the philosophy of separating your components to be container components or presentational components, where there are some that are just focused on what is being rendered and others that have more of that logic in determining what should or should not be displayed.
Debugging errors can be a developer’s worst nightmare...but it doesn’t have to be. Airbrake is an award-winning error monitoring, performance, and deployment tracking tool created by developers for developers that can actually help cut your debugging time in half.
So why do developers love Airbrake? It has all of the information that web developers need to monitor their application - including error management, performance insights, and deploy tracking!
Airbrake’s debugging tool catches all of your project errors, intelligently groups them, and points you to the issue in the code so you can quickly fix the bug before customers are impacted.
In addition to stellar error monitoring, Airbrake’s lightweight APM helps developers to track the performance and availability of their application through metrics like HTTP requests, response times, error occurrences, and user satisfaction.
Finally, Airbrake Deploy Tracking helps developers track trends, fix bad deploys, and improve code quality.
Since 2008, Airbrake has been a staple in the Ruby community and has grown to cover all major programming languages. Airbrake seamlessly integrates with your favorite apps to include modern features like single sign-on and SDK-based installation. From testing to production, Airbrake notifiers have your back.
Your time is valuable, so why waste it combing through logs, waiting for user reports, or retrofitting other tools to monitor your application? You literally have nothing to lose. Head on over to airbrake.io/try/bikeshed to create your FREE developer account today!
JOËL: It's interesting that we're talking about branching, how to structure it, how to model components that deal with branching or that have to be called from a branch because branching is something that I've been thinking about a lot for the past couple of years. One thing I noticed that we tend to do as developers is that we really, really want to force everything down a single path as much as possible.
Like, there is a main path, and then if you have to branch off of it, you go off on the side path. As short as possible, find a way to merge back onto the main path. But there is one single main path in your program, and everything tries to merge back into it. Have you encountered that in your own coding journey?
STEPHANIE: Do you mean having a lot of conditionals along this single path that might take you elsewhere but then, in the end, are just little tangents, and you're trying to get back to that main execution of code?
We have some constructs that make this a little bit nicer. Ruby has the lonely operator that will just sort of keep passing that nil down in a safe way. But it is still doing a nil check at every step of your long chain of actions you're trying to take because that value is potentially null at every step.
STEPHANIE: The lonely operator being the safe navigation operator. Is that right?
JOËL: Yes, that's right. That is another name for it.
STEPHANIE: Okay, cool. Glad we're on the same page.
JOËL: The &. We have some quirky names for operators in Ruby.
STEPHANIE: Yeah, we sure do. I hadn't heard of lonely before, so that was a really cool tidbit for me.
JOËL: There's also the spaceship operator, which is the less than equal greater than for comparing objects with comparable.
STEPHANIE: Ooh, I like that. I've also heard squid operator for interpolating Ruby in ERB.
STEPHANIE: I think, in my experience, I have seen that chaining of the safe navigation operator and the chaining of checking for nil in code that doesn't quite utilize the Tell, Don't Ask principle where you have to check for nil and all the objects down the line rather than having that functionality extracted in a method that is then, in my opinion, more correctly co-located with the relevant domain model. So I'm curious if you think that the conditionals themselves are an issue or if it's just the way that they were implemented or where they exist.
JOËL: In this particular case, I would say the conditionals are a code smell, and they're probably extraneous. You're checking the theme value for null again, or maybe you're checking a derived value for null that you don't need to because you've already checked it earlier. It's code that is not confident because the uncertainty from that initial nil has sort of bled through all of your code.
A classic solution to this problem is to try to push the uncertainty to the edges of your system. And a great resource for that is the book Confident Ruby by Avdi Grimm, which talks about all sorts of techniques for dealing with a lot of those uncertainties that you deal with in code and how to push those to the edges of your system.
STEPHANIE: Thanks. That does actually remind me of what you were saying about componentization and having that outer component make the decision, and then everything else inside of it doesn't need to worry about it anymore.
JOËL: I think it's all kind of connected. I've come up with sort of three, let's call them principles, that I try to use when structuring code that I think are kind of the same idea viewed from three different points of view, or maybe all kind of converge towards the same ideas. The first one being what I showed earlier, the idea of separating branching code and doing code into two separate places. The second one being to try to branch early, push conditionals higher up your decision tree. And the final one being to keep the code within a single method or component or whatever your structuring element is at the same level of abstraction.
So if you're writing at a higher level calling a lot of lower-level methods, that's great. But then don't mix in some lower-level concerns there. Extract those out to a private method or another object or a component and bring those in, and keep everything at a high level in your high-level components, and then everything at a low level in your low-level components.
STEPHANIE: Yeah, that makes sense. I think that is perhaps closer to what I was trying to say earlier, where I think conditionals can be okay if they are in the right place. So if you have a controller and you see a bunch of conditionals, I think if that conditional-checking is related to something like rendering, that feels a bit more okay to me as opposed to seeing conditionals that then execute a procedure or a bunch of different things that might be better extracted somewhere else.
JOËL: I think there are two classes of conditional that you have to think about. Some conditionals are just unnecessary. You're doing extra work that is not required by the code because the code is poorly factored, and so you're having to do this extra work. This commonly happens, I think, in large code bases where they're modified over time, and you get this big, scary large method, maybe with deeply nested code, and you want to just make one modification in it. But you're afraid to break other things, so you wrap it in a conditional. But then everybody else is doing the same.
And then you've got this giant tangle of conditionals, some of which are duplicating each other, some of which will never be called just because it's poorly factored, and it just grew that way over time. And so those, if you're sitting down and looking at cleaning up that code, many of them can be entirely eliminated just by structuring things in a cleaner fashion.
STEPHANIE: Yeah, I've definitely experienced what you're talking about. And I think it does provide a lot of value once someone figures out what the heck is going on with all of these conditionals and wraps their head around it if they're able to refactor it to eliminate some of that complexity that has just downstream effects for everyone working in that code. Like, they don't have to do the work of trying to figure out what is going on, especially for unnecessary logic in the first place.
JOËL: I think a classic case I've seen of this is dealing with wizards where you have a bunch of different steps, and they might be all handled in one place. And a classic way that I've often seen people attempt to do this is say, well, there are a lot of things that might be shared between different steps. Or, again, we want to do this one single linear path. And so you might have, say, one giant Rails controller that accepts inputs from all the possible steps in the wizard. And then it will just say, if this parameter is present, do this action; else, if there is other parameters present, do this action.
It's not even like do this step one action or do the step two action. It might be if the user's name and email are present, then save some data to this table, else if a phone number is present, trigger this background job elseif all these things. But what gets tricky is then you don't know which combinations can happen together.
And then later on, when this gets really big, and you're trying to modify it, and it's like, oh, the customer wants another field on the screen that shows the phone number. But maybe you don't want a background job to be triggered in that case, or maybe it shows up on a different page that you also want to show the phone number on, but now you want the behavior to be slightly different for both of them. And so it gets into this really big tangled mess.
It's also impossible to read that code and know what is going to be executed for each step. So my general preferred approach for that kind of situation...and actually, we have an older episode of The Bike Shed where Steph and Chris discuss this in detail, and their recommendation was similar. So the trick is to branch early, and instead of having a single logical path, it's just check condition, do a thing, keep going. Check condition, do a thing, keep going.
You have branching at the top level that says, if step one, do the step one things; if step two, do the step two things; if step three, do the step three things. And you can have shared logic between them. You might have some private methods that call each other. And all that is fine. You can have levels of abstraction, all the goodies that you're used to.
But now you have a much simpler branching structure because you branch once at the top level. And that might be a four, or five, six, seven-way branch, which is complex. But there is no more branching down below it. After that, it's five or six linear paths going down instead of one giant path with a bunch of branches on it that merge back onto the main path.
STEPHANIE: Speaking of condition with multiple branches, I think we also talked a little bit about this in a previous episode you and I did on case expressions where you talked about how you handled that wizard with a flat case statement. So if folks want to hear more about our opinions on case expressions, I definitely recommend you check out that episode.
JOËL: One thing that I think is really interesting is that when you have extra if...else expressions that you don't need, and maybe they're nested in a certain way, or they're just like really long, you create more paths through your decision tree if you were to model this as a decision tree, then you actually want...so going back to the case of the wizard, the way that you structure it with a case expression is there's one, let's say a five-way branch, and then after that, it's just linear paths. So there are five unique ways to traverse that decision tree, which is exactly the number of ways that you want.
In the original implementation that I talked about where everything is an independent condition that says if this param is present, do a thing, keep going. If this other param is present, do a thing, keep going. And any combination of those might stack up together. Well, now we've got a combinatorial explosion because what if the phone number is present but also the first name and email? Do we do all of those things together? And so it's hard for the reader to understand because there literally are a lot of paths that can happen. And many of them are invalid paths. They shouldn't happen.
STEPHANIE: Yeah, I don't want to be anywhere near a combinatorial explosion based on that term. But, yeah, I think it's also very descriptive of what it feels like to have to parse through a bunch of nested conditionals like that to figure out where you are or what is going to happen next.
JOËL: I mentioned earlier on this podcast that I've done a lot of work with the Elm language. And when they're designing types in their community, they often use the expression make impossible states impossible. And so they'll look at the data structures that they're using and ask, "Are there ways that this data structure can be used to represent values that don't make sense in my domain? And can I change that representation, the definition of these data structures such that it now becomes impossible?" There are some heuristics that you can use to try to make that happen.
There's also a bit of a more mathematical way to think about it, which is thinking in terms of cardinalities, which is how many different types of values can be expressed by a given type. So you think a Boolean can only be one of two values, true or false. That is a type with a cardinality of two. You can do this exercise with different primitive types. But also, once you start combining types together, for example, you've got a pair of Booleans. You've got two values, each of which could be in two different states, and so now those two cardinalities multiply. You've got four possibilities for a type that is a pair of Booleans.
This becomes a really interesting analysis when you start thinking about using this to model a state of your application. So let's say you're trying to model something that has three possible states, and you say, oh, I'm going to use two Booleans to model this. It's problematic because two Booleans have four states, but the thing you're trying to model has only three. And so now you're absolutely going to get in some weird invalid state for that one extra combination that you didn't account for. Maybe that's false and false.
I see that happen a lot, even in database design, where you have two Boolean flag columns that interact with each other. And it's like, oh, but they should never both be false because that's some error state that should never happen, and, of course, inevitably, it does. What was really exciting to me was thinking about this mantra of making impossible states impossible. Can we apply that to branching?
In the way that I've structured my code, there should be the same number of possible branches through my decision tree as there are actual paths through the domain that I'm trying to model. So if it is a wizard with five steps, I want my decision tree to have five paths. If my decision tree has more than five paths, then maybe that's a sign that I need to refactor the implementation because I now have some extra invalid paths that I need to trim.
STEPHANIE: I think the phrase making impossible states impossible is really interesting because that mindset would be really helpful to avoid that defensive coding. I think that shows up as all of those unnecessary conditionals and checking for nil values because you just don't know, even though logically, you might know that it's not possible based on the domain or the business logic.
But we all have seen that no method on nil error come up in our error monitoring service. And you're like, oh shoot, I have to fix that. And you reach for it using that safe navigation operator. And so yeah, the idea of writing confident code, not defensive code (They're opposites to me.), is definitely something that I want to keep in mind.
STEPHANIE: Yeah, absolutely. I think it also shows that a lot of these things are universal. Even though there might be different paradigms, a lot of them kind of, like you said, are enriched by knowledge from other philosophies or frameworks, or it all kind of converges.
JOËL: There's a famous quote, and I've seen it attributed to many people, so I'm not even going to try. And it goes something like this: "History may not repeat itself, but it certainly does rhyme." And I feel like maybe we've got a little bit of that going on here in that the problems and solutions might not exactly replicate across languages and paradigms, but they certainly do rhyme.
STEPHANIE: That's a very, Joël thing to bring up, I think.
STEPHANIE: Classic pulling from history to explain the present.
JOËL: On that note, shall we wrap up?
STEPHANIE: Let's wrap up. Show notes for this episode can be found at bikeshed.fm.
JOËL: This show has been produced and edited by Mandy Moore.
STEPHANIE: If you enjoyed listening, one really easy way to support the show is to leave us a quick rating or even a review in iTunes. It really helps other folks find the show.
JOËL: If you have any feedback for this or any of our other episodes, you can reach us @_bikeshed, or you can reach me @joelquen on Twitter.
STEPHANIE: Or reach both of us at email@example.com via email.
JOËL: Thanks so much for listening to The Bike Shed, and we'll see you next week.
ANNOUNCER: This podcast is brought to you by thoughtbot, your expert strategy, design, development, and product management partner. We bring digital products from idea to success and teach you how because we care. Learn more at thoughtbot.com.Support The Bike Shed
Deploy fearlessly and fix bugs faster with Airbrake Error & Performance Monitoring. Airbrake notifiers are available for all major programming languages and frameworks, and install in minutes, with an open-source SDK-based install and near-zero technical debt. Spend less time tracking down bugs and more time developing. Visit Frictionless error monitoring and performance insight for your app stack.