00:00:10.120
Well, it's a day of firsts! That's my first ever walk-on music. They didn't let me pick a track.
00:00:19.640
Welcome! I'll give you just a few moments to sit down.
00:00:25.800
Alright, time’s up! Hi, I'm Justin. You can find out all about me and everything that I do at my homepage, justin.s.co, or email me at [email protected].
00:00:31.640
Hopefully, it's pretty obvious today is a big day! It's been 10 years since my first Rails World talk. I spoke at Rails World negative 8 in 2014 in Chicago.
00:00:42.160
My career really peaked at Rails World negative 5 when I was invited to keynote in Phoenix in 2017. I'm really enjoying Rails World 2 so far, and I hope you are too.
00:00:50.039
You might remember me for having co-founded Test Double with Todd Kaufman back in 2011, and amazingly it has grown to be one of the most trusted Ruby and Rails consultancies in the world.
00:01:02.440
If anything I talk about today sounds like something you'd love to see with your team, please reach out, and let's talk about how maybe Test Double can help you.
00:01:15.520
Last week, I also celebrated the one-year anniversary of Searls LLC, where I serve as co-CEO with my brother Jeremy. That makes me the Chief Justin Officer, responsible for most of the Justin-related things in the business.
00:01:26.400
This is Becky, sitting right there. She believes in fitness and started a business a couple of years ago called Build with Becky.
00:01:33.200
How it works is that she builds strength training programs for people and then distributes them manually to all of her subscribers, which quickly presented a scaling issue.
00:01:42.160
Like a lot of founders, she decided to scale with software and expanded the brand to incorporate other potential services, which is now actually called Better with Becky. I guess that means we're making a platform play!
00:01:55.960
Becky is not a programmer and couldn't afford to pay someone to build it for her, so that's how I got recruited as her first CSO.
00:02:01.320
If you're not familiar with fitness industry jargon, that means 'Chief Supportive Husband Officer.' So, I built her an app, and that's what brings me to this stage to talk to all of you today.
00:02:14.000
A few years ago, DHH declared that Rails was the 'one-person framework' because it's so productive that even just one person can do something great.
00:02:21.200
I'm one person, so I figured I should use Rails for this app. In this presentation, I'm going to talk about some of that. I really treated this app development as an opportunity to test David's hypothesis.
00:02:36.519
I wanted to ask how does Rails empower me as a solo developer?
00:02:43.560
At the outset, I told Becky she could have as much of my time as necessary to build the app, but I wouldn't let maintaining it become my forever job. Speaking of endings, I have an announcement: this is going to be my final conference presentation. My life has been transformed by the opportunity to speak in front of people like you for so long.
00:03:07.799
But I’ve decided I need to make room in my life for new pursuits. A lot of preparation goes into this, and it’s just a month of nerves.
00:03:15.000
So next time I clock an anxious person, I’m going to clap in their face to calm them down.
00:03:20.920
Today we're going to talk about the six key muscle groups of the Empowered Programmer and discover different ways that Rails helps us get stronger as developers.
00:03:27.640
The Empowered Programmer, first and foremost, is courageous. Let's begin by exploring how even some of the lesser-known features of Rails can make ambitious features achievable for a solo developer.
00:03:39.120
Let's start by asking: what’s the best way to prioritize a backlog? I prefer something called 'The Cone of Uncertainty.' It looks like this.
00:03:46.400
The beginning of any project is when we know the least, so it has the highest risk. As we build software, things become clearer, and that risk diminishes. The cone gradually shrinks before we reach the point of no surprise, where the work is finished and we can all look back, absolutely no one is surprised because we all know how it went.
00:04:06.720
Different projects have different shapes; you could defer the scary bits to the end or pull risks forward and address them right away. You can probably guess which shape results in the best outcomes. The question is, how do you control the shape of the cone?
00:04:29.960
It requires changing how we measure progress. Is progress how much code we ship or how many features we deliver? In my view, it’s actually about how much certainty we gain as we tackle the riskiest features.
00:04:42.080
Planning starts then, instead of looking at normal priority or what’s in the MVP. Instead, we should be asking, 'What am I most afraid of?' For this application, it was video. There's a lot of hosted video in the app, and if you think of a typical form field, there's just not a lot of risk.
00:05:06.720
Even if you have a photo upload or something, you can probably get away with just stashing that in the database. But uploading, processing, and hosting video, that felt really risky to me.
00:05:19.199
So, here's what risk-first planning looks like: first, identify the scariest feature. Then, make it safe for yourself to make a mess, and then you make a mess. Eventually, you figure it out, and it’s not so scary anymore.
00:05:38.400
That’s why I made an Instagram clone for my wife. I know it sounds ridiculous, but I needed a safe and low-stakes way to learn how to host video. My career has taught me that side quests are one of the most effective ways to learn a new skill.
00:06:04.360
This is what Becky’s Instagram looks like, and this is Becky Graham. Everything that she posts here syndicates to Instagram, so she can create content in a place that she owns without disappearing from the internet.
00:06:10.880
Now, let’s do a quick demo of the admin to show off the Active Storage functionality. I started with a basic form and a file input. Okay, so let’s pick out a video at random.
00:06:36.520
I had no clue what to do when the user clicks save. Fortunately, I didn’t have to worry for too long because I already spoiled it! I selected Active Storage because Rails ships with it since 2016 with Rails 5. It has improved a ton since then.
00:06:58.479
Aaron and I actually paired and fixed a few bugs earlier this year, so it still has its rough edges. The upload story is complex enough that I had to draw a diagram.
00:07:11.440
What happens when you click save? First, the file itself is directly uploaded to S3 using JavaScript, and then the form is submitted to the server, which initiates a background job called 'analyze job.' That job downloads the video from S3 to create thumbnails and processes it as needed.
00:07:37.280
That might sound like a lot, and it is a lot, but you know, video is complicated. Trust me, Active Storage handles all the heavy lifting, and it's really nice.
00:07:58.320
So, the payoff is that when you click save, the video is uploaded and viewable. It may not be super exciting, but that’s not the point— it doesn’t feel scary anymore!
00:08:12.599
Unfortunately, implementing Active Storage in an app is a topic much bigger than I have time for today, so I wrote a post outlining my lessons learned, and you can check it out after the talk.
00:08:28.840
The truth is, it takes courage to slow down when you feel pressure to go faster. Slow makes smooth and smooth makes fast, so yeah, could I have solved this without Active Storage? Sure.
00:08:47.640
But Active Storage helped me overcome my fear because so many other people trust it for their apps. Next, the empowered programmer is resilient.
00:09:07.200
Let’s take a tour of different new features in Rails that make things far more maintainable for developers in the long term. My main goal in life and in development is to minimize future regret. That's why I get on stage.
00:09:40.039
Rails 7 helped me avoid three areas where I’ve had regrets in the past.
00:09:42.000
Let's start with authentication and why I decided on email-based login. Everyone has an opinion on login strategy.
00:09:52.120
You might consider passwords, OAuth providers, pass keys, or email. But when users forget a password, you need a trusted email. If an API goes dark, like, you better hope you have their email.
00:10:05.320
If a pass key is created on an iPhone and the user can't log in on Windows, they’re going to contact support by email, so the lowest common denominator is email. Why complicate it?
00:10:15.640
Sure, something is going to defeat email someday, but it’s not going to be my little app. My favorite feature of Rails 7.1 was a couple of methods: one generates a secure token for a model, and the sister method finds it by token.
00:10:27.760
With just these two, you can implement email-based authentication. It looks a little like this: I can configure a 30-minute token in a model, and in a login controller, generate that token to pass it off to a mailer.
00:10:37.959
In the email, you can render a link with the token as a query param, so when the user opens the email and clicks the link, it triggers a different action on that controller to authenticate the user.
00:10:48.320
It’s so easy that I sometimes question its security, but I’ve been told it’s secure, so please don’t hack me.
00:11:05.320
For CSS, Rails now makes it incredibly easy to adopt Tailwind, which is remarkably easy to maintain.
00:11:13.040
Here’s the app’s login page: note the effortlessly rounded corners. Making a nice-looking webpage is challenging, but the true measure of success is trying to change your CSS three years later.
00:11:24.840
For example, if you have an old form that you forgot making and need to add margin above a button, you might find your markup looks like this.
00:11:34.480
You end up searching through for classes tied to those elements, making changes, and worrying that you've broken something else along the way.
00:11:46.160
The three-step process to updating legacy CSS involves finding the rule, making the change, and then exhaustively testing every page on every device. Not easy!
00:12:01.640
Tailwind replaces this arbitrarily expressive CSS with predictable and familiar utility classes. It allows you to significantly reduce the complexity of CSS rules.
00:12:15.640
Whenever I start a new app, I choose Tailwind, and if you’re upgrading an existing app, you can add it after the fact.
00:12:28.240
Lastly, let’s look at the solution to many of my JavaScript problems: Import Maps. One thing I dislike about email-based login is when I type in my email and realize I’m actually on the registration page.
00:12:40.640
So I have to go click login and then retype the email because I clicked the wrong option. If you want to fix that, you’ll need JavaScript, and surprisingly, I’ve loved building stuff with JavaScript.
00:12:54.399
Early on, I had a rough decade with JavaScript, but lately, I’m back at it thanks to Import Maps. Rails 7 lets you load raw JavaScript modules directly without needing a build step.
00:13:10.200
It’s turned on by default, so there’s nothing for you to run, but if you’re upgrading an old app, you can run the installer and add stuff.
00:13:24.000
A few tips for newcomers: you can vendor packages by running import map pin, which will fetch and put it in source control.
00:13:36.120
Make sure that you serve those assets on a CDN with HTTP/2 since there are many tiny requests that would otherwise be TCP requests.
00:13:50.720
Committing packages feels a bit gross, so it’s a healthy pressure to depend on fewer third-party JavaScript libraries. No build is simpler than no build, and you'll have a lot less to worry about already three years from now.
00:14:04.240
Thanks to enhancements like these, I’m confident that future me will be able to maintain Better with Becky for years to come.
00:14:21.120
The empowered programmer is also efficient when conventional approaches would slow your users down. Rails lets you go off the rails and take shortcuts.
00:14:37.200
I’ve always hated when things take too long, and this project revealed opportunities for improvement in Becky’s workflow.
00:14:50.280
Becky has spent a lot of time each month trimming videos, creating documents, fighting with spreadsheets, and emailing things out manually. My sense was she could achieve all the same goals in a whole lot less time.
00:15:06.760
One way I save her time in the app is by turning off a bunch of validations. This is a list of Becky’s workout programs, and instead of validating at save time, I built a custom Readiness Checker that gives a red-green status check of whether that program is ready to roll out to production.
00:15:24.320
Becky prefers to go bottom-up when she starts a program; she dives straight into the workout, and the program upon which that workout depends gets created automatically.
00:15:38.839
Additionally, when she goes to the first block of the workout, it happens again—now the workout relies on that block to be autocreated.
00:15:54.080
Any good Rails developer would look at that and say, 'Persisting stuff when you click a link is not very RESTful!' But everything in life is a trade-off.
00:16:06.240
To me, the user experience matters more in this case. It's not enough to just get out of your power users' way; there are things you can invent to supercharge their productivity.
00:16:18.240
For example, I’ve got these Hotwire combo box lookups where she refers to an associated model in a form. A little stimulus controller I wrote generates a link that opens the edit view of that same model in a new tab, so she doesn’t have to search for it.
00:16:34.800
When writing by hand, Becky abbreviates everything, so we leverage that by supporting her shorthand syntax when searching for movements. I don't know what this means, but it doesn't matter because this app isn’t for me.
00:16:49.520
If you prioritize something like this, your users will love you—even if they're not married to you! To be honest, my favorite feedback is that I’ve saved somebody time.
00:17:02.560
Because Rails doesn’t mind when I start off going off-road, my customers can get more done with less work. The empowered programmer is also dextrous.
00:17:14.240
If Rails' default would zig where my application needs to zag, it’s great that Rails makes it easy to swap things out. Knowing when to break from conventional wisdom is important.
00:17:30.240
Cal, Docker, and self-hosting are all things I won’t talk about today because I’m more than happy with Heroku. My architecture starts with the Heroku app server.
00:17:44.560
It’s backed by their managed Postgres, I use S3 and CloudFront to serve assets, and also use AWS for sending emails. I actually had to look that up because I set it up in January and haven’t thought about it since, so that’s a good sign!
00:18:00.560
Another unusual thing about this app is that it goes out of its way to avoid storing user data unnecessarily. Most apps would store everything by default; every set, rep, and weight would be separate database rows.
00:18:15.440
They’d probably realize they never have much use for that data and watch their costs skyrocket in proportion to usage. I've seen too many curves where revenue starts picking up, and costs start ballooning.
00:18:29.480
As programmers, we add complexity based on assumptions of what others need, so I try to listen to how customers speak about the features they want. It reveals opportunities to simplify.
00:18:46.240
When Becky talks about working out, she emphasizes mind-muscle connection and setting weights based on factors the app can’t know about, alongside a perceived level of effort rather than a prescribed amount of weight.
00:19:01.280
One day, I asked her what the app would do with all these weights that people are entering, and she couldn’t think of anything. So I got rid of it.
00:19:15.640
The new approach is that the app only tracks ephemeral state during a workout. It throws it into a JSONB column and, after the workout finishes, exports the important bits permanently and deletes the rest.
00:19:32.520
The JSONB looks like this: some stuff I need to remember until the next workout, some that I want to remember forever, and the rest can be discarded safely.
00:19:47.320
Considering the new app's data usage, I’m confident that once we generate a ton of revenue, costs should remain mostly flat. But if you all want to subscribe simultaneously and test that theory, that’s fine by me!
00:20:01.600
Good design means throwing overboard things that other people assume they need. Now, regarding system tests, another unusual thing I do is that I’m using Playwright instead of Selenium.
00:20:14.720
David recently wrote a post talking about how sporadic test failures are a massive drain on productivity. Selenium itself is very slow, and you might realize that this slowness encourages us to write brittle assertions.
00:20:30.000
In system tests, a driver automates browser actions and makes assertions waiting until the browser finishes doing stuff, satisfying the assertion before the test script continues.
00:20:48.360
If Selenium is slow during development, assertions may execute after the browser finishes, resulting in a flaky test.
00:21:05.560
If that pass occurs locally while writing the test, you won’t know if it passes before the browser action takes place.
00:21:20.080
The ironic cause of flaky builds is that Selenium actually runs faster in CI because it's headless. Fortunately, Playwright exists as a robust alternative, operating at a lower-level protocol.
00:21:35.440
The short version is just use Playwright. You can install that gem and change three lines to see all the test failures; those are all your flakes just failing for you now, so go fix them.
00:21:54.480
Since switching, the only time my build has failed is when Stripe checkout goes down, which is separately concerning but not my problem.
00:22:08.880
Once you work on a few apps, you find yourself much more comfortable swapping things out as needed. The empowered programmer is also disciplined.
00:22:19.760
We can learn from Rails by being more opinionated in our design. Focus on building only what matters and ignore the rest.
00:22:39.240
I believe that good software gets updates forever, but great software is designed, built, and then just works. We need more practice learning how to say no.
00:22:59.640
Vision is the easy part. I can have a brilliant idea in the shower, but executing that vision requires months of constant vigilance and saying no to distractions.
00:23:06.720
For example, I was adamant that we limit user-facing features. No to a more tab, hamburger icon, or ellipsis button in the UI.
00:23:22.440
Instead, I pulled out a whiteboard and drew the three tabs I knew we needed, turning it around to Becky to let her pick wisely because she only gets five.
00:23:35.040
We eventually landed on those answers, and the tab bar looks like this with the five tabs. If you look now, the same tabs apply.
00:23:44.440
So, we have five in February, five in June, and five in September. Yes, that was arbitrary, but this is what people mean by a liberating constraint.
00:23:50.800
It strengthens the product. This is what the membership program summary looks like. While it took me all afternoon, I used CSS grid to create this pretty little summary of a workout.
00:24:06.000
When I showed it to Becky, her first reaction was, 'Can I have a PDF version?' Before I could respond, she said she actually needed a second version of that PDF too.
00:24:26.720
Exhausted at that point, I immediately said no, then tried to find a reason. I said I didn’t want to deal with fancy PDF engine maintenance.
00:24:39.440
Fortunately, CSS makes this possible with media queries. Tailwind makes it easy too; it has a print variant where you just say print colon and any utility class.
00:24:54.800
I was delighted to find out that within 30 minutes of using print colon, I could make the printout look good enough for Becky, and then the project schedule wasn’t derailed.
00:25:09.680
Another day, she mentioned users need to search for movements and equipment, which is reasonable, but I was drunk with power at that point.
00:25:19.560
I reflexively replied, 'No fancy search features, no pagination, no infinite scrolling UI stuff!' Instead, I said, 'Let’s load them all until it poses a problem.'
00:25:33.919
This actually worked because there are only several hundred things, and it only took a few adjustments like lazy loading images.
00:25:49.679
If you’ve never lazy-loaded an image, here's a tag doing so—very simple! It’s worth doing.
00:26:03.760
The second thing I did was utilize fragment caching in the view layer. Secret: I’ve never used caching in Rails since I started playing in 2005, so this was brand new to me!
00:26:15.640
If you're iterating over a relation of movements, you just wrap it in a cache block, passing the thing that you know defines the cache key, and that's about it.
00:26:31.280
However, there’s one thing to note: whenever you have a belongs to association that you want to invalidate, you need to add touch: true on it, so the updated at changes.
00:26:43.760
To conclude my knowledge on Rails caching, just go with it! Being everything already in the browser means you can easily filter stuff client-side, right?
00:26:58.760
So she could have the search she wanted, and it would actually be faster. This led to yet another simple stimulus controller.
00:27:13.520
On every keystroke, it just stores the query in a variable, and every time it changes, it filters and hides items.
00:27:30.360
Ultimately, I’m happy with where this landed. You can scroll through everything, type in search words, and checkboxes for different movement patterns.
00:27:44.160
One of my favorite features is that if my frontend code or what I'm talking about interests you, you can view source maps in production, allowing you to read and play with all code interactively.
00:28:01.840
You just need to subscribe to the app for $30, which also includes a fitness app.
00:28:15.560
Learning when to say no helps me remain true to my vision, leading to better software through discipline. The empowered programmer is finally meticulous.
00:28:30.880
Rails was created by people who sweat the small stuff, so it’s no surprise it gives you what you need to pay attention to those details.
00:28:44.000
Admittedly, I prefer real native apps with buttons over web apps pretending to be apps, and these fancy gradients don’t make this a real button.
00:28:57.760
I appreciate the little touches that go into software. For example, when making a site a progressive web app, it can own the full screen if someone adds it to the home screen.
00:29:09.760
I was happy to see David merge this, where now PWAs are turned on by default with any new Rails project, but you can do what I did and copy-paste from the GitHub UI into your project to add them.
00:29:25.840
Let’s demo the workout UI. Each set looks like a little card with an instructional video.
00:29:39.840
Users can scroll down to see their past notes for a movement, with controls to complete or skip a set. Many of those tiles are interactive, allowing navigation between blocks.
00:29:53.040
Users can expand or contract videos and see timers—things like that. There’s a lot of fun! When I demo this, people often ask, 'Wow, how did you do that?'
00:30:05.360
Fortunately, most of you are seated because the answer is an 8 kilobyte opening div tag, numerous stimulus controllers, and a whole bunch of data attributes storing the entire UI application state.
00:30:16.160
I’m so productive with Stimulus because instead of managing separate server-side and client-side state, the DOM sits in the middle as the true source of record for everything.
00:30:29.520
First, Rails renders the page. Suppose a user clicks the complete button—this triggers a stimulus action that updates the data attributes, generating a fire-and-forget message.
00:30:43.440
This message updates the data on the server, animates the next set, and updates the current URL. So if you hit refresh, you're in the right place, not jumping around.
00:30:58.720
I know it’s a lot; it was for me too. My generalized advice is: when you're using Stimulus, just do whatever the server would do without JavaScript and keep them aligned.
00:31:11.440
When the user takes action, update the UI optimistically, and persist whatever you need to do via a fire-and-forget message that doesn’t block the user.
00:31:25.120
Finally, ensure at every step that you’re passing the refresh button test.
00:31:29.920
Refresh the page, and you’re not jumping around. A good Stimulus controller is like a good babysitter; it makes the same decisions a parent would.
00:31:38.080
Now, let’s demo a situation that often occurs in gyms where people juggle multiple devices, ensuring they don't perform unnecessary activities.
00:31:46.080
Here's an iPhone and an iPad. Watch the progress bar on the iPad as the user completes sets on the iPhone. You can see it fills in automatically, updating the exercises reflected in all clients simultaneously.
00:32:07.920
If you open that more tab and start typing a note, 'I’m having such a great workout,' as soon as you blur it, it’s also going to appear on every single device.
00:32:21.280
What is this dark magic, you may ask? It starts by attaching to Turbo Action cable using the turbo stream from a helper and in an after-commit hook in the model.
00:32:34.720
Using broadcast render to point out a partial, the turbo stream action is rendered with a tag. Turbo Stream makes monitoring changes to the DOM easy.
00:32:47.760
This stuff is all super cool, but if you’re like me, you might find something that worked once and then forget how to implement it later.
00:33:01.120
When that happened to me in 2006 with Rails, having recipes was helpful as a step-by-step guide. It’s been a while since we’ve had to adopt such a mindset shift.
00:33:15.120
I wrote this little recipe to guide you through implementing similar features in your own apps.
00:33:28.960
Hotwire generally recaptures a lot of the magic of Rails 1.0 while retaining the misery of unlearning previous methods.
00:33:41.840
I continue to be amazed at how Rails makes obvious things easy without making super obscure and detailed things hard, which I, as a meticulous person, truly appreciate.
00:34:04.080
Next time you start working on a new project, ask yourself which of these six traits might benefit from a bit of strength training, and what new features in Rails might help.
00:34:18.640
If you're interested in Build with Becky, you can learn more and sign up at betterwithbecky.com.
00:34:31.200
I won’t claim to be skeptical, but I’ve been weightlifting for a few years and did last month's program from Becky to test the app. I broke eight personal records, so I was pleased to discover it works!
00:34:45.280
Thank you for your time! I’m staring at the end and not planning to make a pun like 'Now that's a load test!' Thanks for the groans.
00:34:56.560
But even if it’s not your cup of tea, maybe you know someone who would be interested. Please tell them about it.
00:35:07.040
We haven’t announced it yet, but we only have about a dozen subscribers so far, and I’m sure Becky would be thrilled to have all of you and your friends on board.
00:35:20.640
Test Double is also here at the conference sponsoring. We've got kind of an office hours awkward space where you can pair program or get some free consulting at the Double Up Lab.
00:35:35.600
The next session started about two minutes ago, so go check it out. I hope to see you there.
00:35:44.560
Thanks for coming to my last conference talk! I guess I'm done doing this, but I can't help but create content.
00:36:00.240
I do have one breaking change to share—just kidding, that's the name of my podcast!
00:36:16.960
As Mike says, if you're still here, there's a chance you don't recoil at the sound of my voice for three hours. If you’re like Nadia, you might subscribe to catch up with what I’m doing occasionally.
00:36:31.040
It’s a laid-back, low-stakes listen with explicit language. Last year, I also started a free monthly newsletter called 'Searls Of Wisdom.'
00:36:44.160
Like Eileen, I generally dislike newsletters, so I strive to make one that's actually good. If I can get Aon to check his email, then I know I’m doing something right!
00:36:56.560
Finally, if you want to get in touch anytime, please email me at [email protected]. I’d love to hear from you—fewer people are emailing than you might think.
00:37:10.240
Thank you so much for being here with me today!