An alternate approach to Avdi’s method of slimming down Rails controllers

I recently watched/read Avdi’s fantastic article Slimming down hefty Rails controllers AND models using domain model events on Ruby Tapas. PS – you should probably subscribe to Ruby Tapas.

While being amazed by Avdi’s refactoring skills and watching him plug in proven software development patterns, I couldn’t help but think about how I would have approached this problem.

Recently, I’ve been trying to be extremely adherent to “The Rails Way.” I’ve been using as few gems as possible, and just working with what Rails gives you out of the box. It’s been an eye-opening experience to say the least.

If you haven’t yet watched the episode, or read the accompanying article, essentially the problem is a long update method in the controller for a project management app. It has a bunch of nested conditionals with methods to send out updates via email or Websockets depending on what has been updated. Because so much of this depends on the logged-in user and other session-specific variables, breaking the functionality out into a “domain object” didn’t make a ton of sense.

What Avdi observed (pun intended) is that these are actually life-cycle events for a task and associated project.

Looking over this action again, we realize something: hidden in all these conditionals is a series of domain-model lifecycle events, each with different concrete actions which trigger on those events.

  1. There are some actions to perform anytime the task is successfully updated.
  2. There are actions to take when the task has moved from one project to another.
  3. There are actions to perform when the task is newly created.
  4. There are actions that happen when the task’s status has changed.
  5. There are actions for when the task has been reassigned.

He then proceeded to add observers to the Task model and subsequently send out notifications to listeners whenever a life-cycle event occurred.

Immediately, I see a lot more objects here. I see a Move, a StatusChange, and a Reassignment; all resources related to a Task. Only a traditional updating of attributes belongs in this method and controller.

As an aside, I’m not exactly sure why handling the case of a newly created task is in the update action of the controller instead of create, but I’ll assume there is something here that we don’t know. For simplicity, I’m just going to skip it.

Let’s start with some routes:


resources :tasks do
  resource :move, only: [:create]
  resource :status_change, only: [:create]
  resource :reassignment, only: [:create]
end

Now, instead of just hitting the update action, anytime something happens with a Task, we’ve got a very distinct route for each special case.

Because the controller in the example is just returning JSON, I’m going to assume that this is a Javascript front-end communicating with the Rails app. This would require a change in the front-end logic to POST to a subresource url instead of a PUT to the task url.

Here’s our first controller to handle the case of when a Task moves from one project to another.


# app/controllers/tasks/moves_controller.rb

class Tasks::MovesController < ApplicationController
  def create
    old_project_id = @task.project_id

    if @task.update_attributes params.require(:task).permit(:project_id)
      push_project_update old_project_id
      push_project_update @task.project_id

      respond_to do |format|
        format.json { render "show", status: :accepted }
      end
    else
      respond_with @task do |format|
        format.json { render @task.errors.messages, status: :unprocessable_entity }
      end
    end
  end
end

Now, let’s examine what needs to happen when a status change occurs.


# app/controllers/tasks/status_changes_controller.rb

class Tasks::StatusChangesController < ApplicationController
  def create
    old_status = @task.status

    if @task.update_attributes params.require(:task).permit(:status)
      notifee = @task.readers(false) - current_user
      if notifee
        mail_completion_notice(notifee) if @task.status == Status::COMPLETED
        mail_uncomplete_notice(notifee) if old_status == Status::COMPLETED
      end

      respond_to do |format|
        format.json { render "show", status: :accepted }
      end
    else
      respond_with @task do |format|
        format.json { render @task.errors.messages, status: :unprocessable_entity }
      end
    end
  end
end

I’ll leave it to the reader to come up with their own implementation of a Tasks::ReassignmentsController but I think you can see how this method works.

I’d also challenge you to start thinking very strictly in terms of REST and “The Rails Way.” This codebase is the perfect example that rarely does an app just have basic CRUD needs. More often, there are special cases, and paths that require a different response despite technically falling under one of the basic CRUD actions.

Think about updating a User in your own codebase. Does the same thing happen when they update their password as when they update their name? Or, are you sending a email in once case and not in the other?

Instead of nesting conditionals inside of your update action in your UsersController could their be a Users::PasswordsController? Start flushing out more and more sub-resources with small controllers for these special cases.

It may not follow the classic patterns, but I’ve enjoyed this method of thinking recently.

I’d love to hear what you think as well!

Why Webhooks Are the Future of Web Integration

Anyone who has ever built a web application has surely felt the need to add features. You need to user management, email, payments, the list goes on and on. It isn’t too long before you realize that you’re re-inventing the wheel time and time again. Even worse, you’re running into problems that have already been solved!

For the sake of this post, I’ll use email as the example. You start with the simple transactional emails to confirm user registration or reset a password, but it isn’t too long before you think, “hey I’d like to allow my users to email all of their customers from time to time.”

Now you need to worry about staying out of SPAM filters, unsubscribes, DKIM signatures, the list goes on and on.

Obviously there is a lot more to this than you realized. Good thing this problem has already been solved multiple times over.

Next, you say, “I’ll just let my users integrate their Mailchimp account.”

So you spend a few days slinging some code, and fully integrate the Mailchimp API. Your users now can send email newsletters to their customers with pretty templates and confidence that they’ll arrive. Life is good.

Then a support request comes in saying:

“Hey, I saw that you now enable integration with Mailchimp. This is so great! I’ve been sending customers emails since I signed up, but have to manually export them. Only problem is that I use Drip. How long before you offer an integration with them? Also, a good friend of mine is a user as well but uses Campaign Monitor, I know she’d love to be able to do the same. Hope to hear from you soon!”

You’re now stuck with dozens of support requests for integration with their favorite email app, or CRM. Plus, you have to worry about deprecations or changes to every API you’ve integrated with. 😱

If only you could force them to integrate with your API. Those selfish developers should offer the ability to poll for changes and automatically update their records. After all, they’ve got millions in VC funding and a team of developers!

Cooler heads prevail, and you realize that it isn’t viable for anyone to keep up with every other app’s API who could potentially want to integrate.

🤔 Then you remember webhooks!

Any time a change occurs in your own app, you can send an update via a webhook to any URL a user wants! Ingenious – let your users decide where and when to send data they want!

A few days go by and what once felt so overwhelming has now simplified into:


# customers_controller.rb

def update
  if @customer.update_attributes customer_params
    current_user.webhooks.run "customer.updated", @customer
    redirect_to customer_path @customer
  else
    render :edit
  end
end

webhook-flow

Now anytime your user’s customers update a profile, they have instant push notifications to their favorite email app, CRM, and analytics tools!

There are even amazing services like Zapier who can take your webhooks and put data into a Google spreadsheet, post a Tweet, and update your Slack room without any programming by your users!

Now you understand why I believe webhooks are the one feature you must add to your API.

Announcing SMS Hooks

I’m going to keep this one short, but I’ve got 2 important things to announce today.

1. Announcing SMSHooks

It’s up, SMSHooks has a landing page! I’ve got a basic pitch, a quick screenshot, and some features spelling out how the service can help businesses.

It’s purposely very basic, but just enough to see what kind of interest there is for businesses who use Chargify, Stripe, and Shopify and want to connect with their customers via SMS.

2. Start Small, Stay Small eBook – A Giveaway

As I mentioned in the previous post, I bought, and quickly read Start Small, Stay Small, the fantastic book by serial entreprenuer Rob Walling.

This book is so chock full of insight, and actionable advice, I want to be able to share it. As such, I will be giving away several extra copies that I bought.

How do I get my copy?!

I offer 2 ways to get yourself a copy of this fantastic resource:

  1. Throw down $19-25 and buy it. Seriously. More than worth the money.
  2. Email me, and give me honest, insightful feedback on my business idea, SMSHooks. I’ll enter everyone who emails me into a drawing and give out a few copies to the winners.
    Note: the feedback does not have to be positive; just honest.

What’s next?

That’s it for this week. I’m happy to have a landing page up, and I’ve already solicited some fantastic feedback. I’m going to continue working to drive traffic to the site, and see how I can help people with their eCommerce businesses.

I’m looking forward to hearing from you, and excited to continue working and sharing my progress.

A Mindset Shift

This undertaking has been much more than I imagined, even already. There’s an old saying that I refer to from time to time.

It’s not what you know you don’t know, it’s what you don’t know you don’t know that will get you.

Shifting My Mindset

Over the weekend, I read Start Small, Stay Small (I also bought a few extra copies that I’m going to be giving away), and so much of it really blew my mind.

Focusing On the Product

Here’s a common scenario ever since I learned even the most basic programming task. I’m sitting at work or at home using a piece of software, and I think to myself, “I could totally make this better.”

I know, completely arrogant, and a little ridiculous, but tell me you haven’t done the same! Tell me you haven’t sat using Quickbooks for some reason and thought to yourself that if you ever had the time, you could write a better solution for business owners to keep their books.

What’s the second thought?

“Intuit made $4.2 BILLION dollars in 2015. Imagine what I could make with a better product?!”

I start thinking of the features Quickbooks has but doesn’t need; what it needs but doesn’t have; how unintuitive (no pun intended) the interface can be. Give me an uninterrupted year, and I’ll meet you at the billionaires club.

Make a better product, and people will line up to pay for it.

I know it’s unfair to disparage a perfectly good and extremely profitable company, but my point is that I’ve always been focused on the product. I imagine what database tables are needed, how you would code little portions, what the menu headings should read, etc.

Never once do I think of marketing, or advertising, SEO, business partnerships, or sales teams. After this weekend, I realized that this is why Intuit is so successful. They put the lion’s share of their time and energy into getting people to buy their software.

Learning What You Don’t Know

It has slowly been sinking in, but it’s hard to argue that the product is the least important part of running a business. If people don’t buy your product, who cares how much better it is?

If I’m being honest, here’s how I really imagined this process going. Code up an app, email some people, throw them a free invite, get a mention or two from prominent bloggers, and watch the service grow. If the product is superior, people will talk about, tell their friends, and everyone will switch over.

Now I have a glimpse of why I saw a fat ZERO for sign-ups when I’ve launched products before.

I’ve only been vaguely aware that SEO, search keyword rankings, marketing, advertising, sales funnels, and partnerships exist, but have had no clue how important they are to your success.

This is worse than learning a new programming language, or database, because I don’t have the transferable skills or vocab to even understand the concepts right now. I have full confidence that I’ll learn it quickly, but I just feel like I keep putting more books into my Kindle queue.

At some point, I know I have to just say that I know enough and get to work, but I want to give myself a better chance of success from the jump.

My first step towards success is a realization that I have to focus less on the product. I have to become the sales guy, the SEO guy, the marketing guy.

I’m learning a lot and feeling a mix of being completely overwhelmed with how far I have to go, but at the same time being excited about a new challenge and learning experience.

Any Tips?

Got a favorite book or blog that will help? Know the best SEO girl in the biz? Share your thoughts below or shoot me an email.

As always, I appreciate you sharing your own valuable time to read and interact. If there is anything that I can help with, please reach out.

Realizing Your Value

I want to share a quick story, and how it really smacked me across the face with a realization of how often we leave money on the table as developers. I hope that by the end of this post, we’ll all feel less weird/guilty/embarassed about charging an appropriate amount of money for the value we provide.

When thinking about charging money for my skills, there are too many instances I completely disregard the value of what I’m able to do.

What I can do in an hour, may take someone years to accomplish.

How many hours have we all spent learning to code, reading books and blogs, watching screencasts, pouring over APIs?

Don’t forget these! We have invested the time, energy, and money in ourselves. And why do we make investments? For future payoff!

With that, on to my story.

A Call List

A guy I know helps run events, and like most events, people wait until the very last minute to confirm whether or not they’re coming. Despite countless emails, cough, cough, some people just don’t act.

So, a few days before, he ends up having make phone calls to everyone to ensure that they received the message that the deadline is very quickly approaching. He doesn’t like dropping people from an event without providing every opportunity for them to confirm. Nothing like having an angry participant yelling about how they didn’t receive any notices of a deadline because they don’t check that email address anymore.

So, anyway, he’s telling me how there’s a huge list and how he’s going to be paying a few people to sit and make phone calls all weekend, and into the next week.

He’s not exactly great with computers, and as I’m sitting there listening, he also asks me for some help formatting an Excel spreadsheet so that the callers can be more efficient with calling and marking whether or not the call was answered.

“Sure. Email it over, and I’ll clean it up before I leave today.”

A Wizard Appears

So, I’m sitting there looking at this list of just under 1,000 phone numbers and thinking about how long these people are going to have to be sitting there calling each person, and repeating a script.

As a fellow developer, I know what’s burning you up inside right now… This is the ideal case for automation.

I have a Twilio account and a few minutes. I pull out my iPhone, record a voice memo of the information each person on the call list needs, and within no more than 20 minutes total, I shoot off an automated call.

I also included my colleague’s number…

A few minutes later, he walks in with a confused look on his face. He received the phone call.

“How did you do that…?”

“Oh, well… Everyone on that list received the same message. All of the calls are done.”

I may as well have made a 737 jet disappear; the magic was the same in his eyes. I had achieved wizard status.

The Fail

The next question is, what should we charge this person? The total Twilio fees ended up at about $11.00 or so, so I figured $20.00 or $25.00 is good. It didn’t take me much time or effort, and that more than covered what I’m actually paying in fees.

Fair right?

WRONG

This person was ready, willing, and happy to spend hundreds of dollars to pay a few people to make all of these calls for him. I just saved him countless hours, and quite a bit of money. As quick as I said $25.00, I could have said $250.00 and he wouldn’t have flinched for a second.

If I spent some time and really broke down how much time and money I would save him, I could probably convince him to pay much more than even $250.00.

Understanding Our Value

So, what’s the true value of our work? The mistake I continually make is to sit and start thinking of an hourly rate, how long something will take, what’s the “fair” price to them, will they think I’m asking too much, etc. There’s a flood of questions I ask, when the answer is very plain and clear.

My work is worth whatever someone is willing to pay.

Most people I know aren’t willing to just give their money away. I know I’m not. We all calculate what something is worth to us, and if it’s worth it, we pay it. We make our decision based on the value a product or service provides us.

My guess is that I could charge $100.00 a month to automate these calls. I could write the script once, spend $11.00 in Twilio fees every now and then, and end up well ahead at the end of the year.

If I came across a website offering call services at $100.00 a month, I’m passing without even considering it. The value isn’t there for me. I could do it myself for much less, with very little time invested.

But, I know of at least one person already who would be entering his credit card this second at $100.00 a month for this. The value of automating these calls is well worth $100.00 a month to him. By paying for this service, he ends up ahead in money and time, a great value.

Wizards Don’t Pay for Magic!

The next time you’re ready to undercharge, or do something for free because it’s “easy,” remember this. Just because this magic flows through your hands typing on a keyboard, not everyone has been trained in these spells. What you would be utterly unwilling to pay, may be a great deal for someone else.

Anytime you can alleviate a pain point, save time, and save money for someone, the value you provide is whatever they’re willing to pay. If you can gain someone one more customer who provides a lifetime value of $1,000, then charging $500 is a great proposition no matter how much time or effort it takes you to accomplish the task.

No need to feel weird charging a fair amount when both parties benefit.

What About You?

Have you ever left money on the table because you failed to charge based on the value you provide?

Please share your story in the comments, or shoot me an email.

Week 1 Review

Week 1 is in the books, and I really can’t believe it’s already Friday! I’m consistently amazed at how fast time can go, and how poorly I still estimate how long something will take.

Without further ado, here are the answers to the questions I’m committed to answering weekly.

1. What did you do this week to make your business better for your customers?

This is a difficult question to answer at this stage because the “business” doesn’t really exist. I’m still in the validation phase, and trying to flesh out that I’m going to have a product to sell.

In the short term, that’s a non-answer, but I think it will make things better for the business long-term because I’ll have a better idea of what customers want and need.

2. What have you done to acquire more customers than last week?

As mentioned in my last post, I’m actually talking publicly about the business. I’ve emailed a few people in the space, gotten good feedback, and even got a nice tweet from Alex Turnbull of Groove. This was really exciting for me as the Groove blog was really one of the biggest inspirations for me to start this project.

I also had an interesting bit of feedback on the level of my goal from Peter Cooper — a prolific educator and curator for anyone unfamiliar. His take was that the goal of $1,000 in MRR is too low. He felt that by shooting for a more ambitious target, I may fall short but still land beyond my initial conservative hopes.

This is really something I’m still thinking over. On the one hand, I want to be conservative and have a realistically achievable goal. On the other, I’m still a dreamer and believe that this could turn into more. I’m definitely willing to move on this, but want to hear your opinion as well.

3. What did you do well that you should repeat?

I’m getting more comfortable cold-emailing people, and just putting myself out there. It can be intimidating to offer something up that you’re unsure about, but I’m starting to realize that there really isn’t any downside. Any potential embarrassment I thought I might have felt hasn’t been felt at all.

4. What did you do poorly that you should reduce?

I need to make it easy for people to offer feedback without taking too much of their time. I started to get discouraged when I would email someone and not get an instant response, or when I would publish a post and ask for “feedback” and not receive any.

First – Unless you’re on the mailing list, it wasn’t simple or obvious that I would love to have an email from you. I have since added my email address to the blog layout, and will start asking for emails.

Second – I added Disqus comments to each post to allow readers to post anything they’ve got there.

Finally – I need to start asking specific questions, and offering a way for people to respond quickly and easily. “What do you think?” or “What’s your feedback?” aren’t great questions.

5. What do you hope to do by next week?

I want to find someone who says whether they would pay for this product. I want to reach out to a few firms who develop with Shopify and Stripe to solicit their feedback. I need multiple people to tell me, “yes,” “no,” or “maybe.”

Everything in my bones wants to hop into a console and type: rails new but I’m going to resist until I know that a market exists.

Bonus: What can I do for you?

I’ve spent a lot of time asking things of my reader, but I want to make sure that I’m answering the questions you have. Want to know how I come up with business ideas? Want to know where I’ve fallen on my face before? Want me to help validate your ideas?

Email me! I’m committed to reading and responding to each email I receive (for now).

That’s all I’ve got this week, but plenty more to come.

The Opposite

Confession – This isn’t the first time I started to work on a side project. Like most of you, I have moments — sometimes daily — where I think of a great new idea and imagine it taking off.

Tell me if you’ve ever gone down this path:

  1. Have an idea.
  2. Get super excited.
  3. Lock yourself in a room and start coding. Implement all the awesome features that you’re sure everyone is going to need and love.
  4. Think about telling someone about it, but not feel comfortable because the product isn’t actually “ready.”
  5. Get discouraged because you have no traction (even though nobody knows about it).
  6. Dream about what it would be like to run your own successful business and think that the reason you failed is because the idea was no good to begin with.

…raises hand

I’ve done it. More than … more than twice.

I’m now commited to doing the opposite.

Today, I am telling everyone about my idea without having written a single line of code.

Notifications

In the course of my day job, I work with a lot of younger people; high school, college aged. Over the past 5 years or so, one thing has become more and more apparent: If you want them to know about something, the last thing you should do is email them. Half of the kids told you an old email that they now haven’t checked in 2 years, or they only check it a couple of times a month. Bottom line, email isn’t what they use.

I’m quite the opposite. I use email extensively personally and professionally. I do beleive that as these youngsters grow older, email will become more ubiquitous in their lives, unless of course Slack conquers the world.

Even as an email lover, there are times that it is not the best media, namely notifications. I’ve found that SMS messages work far better for getting information to people quickly and ensuring that they actually see it.

Like most everyone else in the free world, I order a lot online from Amazon. One of my favorite things about the Amazon iOS app is the native shipping notification. It doesn’t require any action, but just tells me that my order has shipped, is out for delivery, delivered, etc.

What if every store could provide real-time, customizable notifications via SMS instead of relying on installation of a native app.

SMS Hooks

So, here’s my big idea. I want to integrate with Shopify, Stripe, Chargify, etc. and provide developers, store owners, or anyone else the ability to quickly and easily pre-make SMS templates and automatically fire them off as events happen.

Scenario:

I order a new pair of tube socks from an online store. Immediately, I get an SMS message:

Hey Kyle! Here's your confirmation #123. Thanks for shopping with Awesome Socks!

A couple of days go by, and boom! Awesome Socks shoots me another…

Good news Kyle, order #123 just shipped! Thanks again, we're thrilled you're our customer.

Quick, simple notifications that I can read immediately and move on with my day.

Scenario #2:

I run a SAAS app, and a big customer’s card is declined when trying to renew. My phone buzzes with the following:

Big Time Customer, LLC's subscription renewal just failed. Your MRR will decrease by $500.

At the same time, Big Time Customer receives:

Your monthly subscription to My Sweet SAAS, LLC has failed. Please check your card information at https://bit.ly/QDh37c to ensure that your serice is not interrupted.

As the SAAS operator, I can now potentially avert a big drop in revenue and ensure that my customers never see a drop in their service, all because I got notified immediately and it didn’t get buried with the other hundred emails I already receive daily.

Scenario #3:

Just kidding, I think you get it. You build SMS templates to be triggered by an event.

No need to worry about old email addresses that aren’t checked any longer. No need to worry about bounces and whitelisting IP addresses to ensure email delivery. Quick, push notifications to the device that most people now have on them at all times, without the need for any app installation.

Time for feedback

OK – I’m ready for it. What do you think?

  1. Sounds awesome!!! When can I put my credit card down?
  2. I don’t know Kyle… sounds a bit far fetched. What else ya got?
  3. Seems OK. Let’s see how this plays out.

Seriously, I want to know what you think. Would you pay for this for your business? Would you set this up for your clients?

My experiement depends on you helping me validate my idea before I dump hours and dollars into building the software.

Thank you!

I’ve already heard from a few of you, and I sincerely appreciate the engagement. I hope to hear from all of you over the course of this venture, and that we can all share ideas to help one another achieve our goals.

The Side Project

The Side Project, or how I went from $0 to $1K MRR.

It’s been a while since I’ve posted to this blog, but today I am excited to start a new chapter and begin a series on, The Side Project.

“What in the world is he talking about?!”

Glad you asked! I’ve decided to go all in on a side project and want to document the process from start to finish. It’s something I’ve thought about for a long time, and I will put it off no longer.

Inspiration

Like most of you, I read a lot of blogs, listen to podcasts and always dream what it would be like if I were the driver’s seat.

In the spirit of Journey to $500K, the excellent business blog by the good folks at Groove, and the new direction that the boys at Thoughtbot have taken with their Giant Robots podcast, I want to document and share my journey of trying to build a viable business online.

The posts will consist of code, successes, failures, and the resources and products I use. My goal is to build a community to help hold each other accountable in our ventures, and provide usefull feedback to help us all achieve our goals.

Short term goals

My goal by January 1st, 2017 is to have a side project launched and have at least 1 paying customer. As a side project, I cannot devote 100% of my time to it, and we all go through periods of varying activity. I think several months from now is a reasonable expectation to have a product launchged.

Long term goals

As you may have guessed by the title, my goal by January 1st, 2018 is to have a net $1,000.00 in monthly recurring revenue. I can hear a lot of you at there already…

“That’s not really a lofty goal for a year and a half from now…”

You are right! But, it’s also what I consider realistic for a side project, and an extra $12,000.00 a year can make a big difference in most of our lives.

For me, I’d like to use the new money to fully fund my Roth IRA and contribute to our son’s 529 education account. I also think that a $1K MRR business has proved to be viable, and could potentially grow into more, but I don’t want to put the cart before the horse, as they say.

Format

I will post weekly updates on what I’ve done, what I’ve learned, and what comes next. I’m going to blatantly steal from the Giant Robots podcast and answer these questions every week:

  1. What did you do this week make the business better for your customers?
  2. What have you done to acquire more customers than last week?
  3. What did you do well that you should repeat?
  4. What did you do poorly that you should reduce?
  5. What do you hope to do by next week?
  6. What is your MRR?

What can I do to help?

The number one thing to help me is to be engaged, offer feedback, and share with your friends. The only way I can hope to succeed in my goal of learning more and bettering myself is to hear from others. I promise to read all emails, and respond to most.

I’d also love to have you keep me accountable to answering the 6 questions above. If you haven’t heard from me by Friday, find out why!

What’s next?

I want to hear from you! I want to know where you struggle, what you want to hear about, what you think of my idea. Anything you’ve got, I want to hear it.

I have the idea for my product, and that will be shared with people on my mailing list later this week. If you’re not already, please sign-up for the list below.

I’ll be offering exclusive material and engagement with those on the list, and have a couple of other benefits in mind that I don’t want to disclose just yet. I promise to keep your email to myself, and never SPAM you with anything.

Hope to hear from you all soon!

Bulk Tagging in Rails

Recently, I started using Postgres’ native array type to tag records in an eCommerce application. It was huge upgrade to move to this solution instead of having multiple tables storing related Tag records.

I’m going to pass on the “Intro to tagging” type post here, because it’s already been done better than I can.

But, there is an area that I have not found covered; bulk tagging.

For the sake of our article let’s assume that you’re running an online store, with a Product model that has a tags attribute. Periodically, you want to mark products as being a “sale” item, or a “best seller.” How can we add tags to multiple records at once?

A ProductTagger

Let’s throw aside the UI component for now, and just focus on our code. For the remainder of this post, we’ll build a new ProductTagger model to bulk tag records. We can start with 3 basic requirements:

  1. The tagger should add the tag to all records.
  2. The tagger should not add the same tag more than once.
  3. The tagger should not set each record to having the same set of tags.

As I have in previous posts, let’s also start with some failing tests that lead us to where we want to go. We’ll include 3 tests for the basic requirements described above.


# test/unit/product_tagger_test.rb

class ProductTaggerTest < ActiveSupport::TestCase

  def setup
    @shirt  = Product.create
    @shorts = Product.create
    @tagger = ProductTagger.new @shirt.id, @shorts.id
  end

  def test_tags_added_to_all_records
    @tagger.tag 'sale'

    assert_equal %w(sale), Product.find(@shirt.id).tags
    assert_equal %w(sale), Product.find(@shorts.id).tags
  end

  def test_tags_are_not_duplicated
    @tagger.tag 'sale'
    @tagger.tag 'sale'

    assert_equal %w(sale), Product.find(@shirt.id).tags
    assert_equal %w(sale), Product.find(@shorts.id).tags
  end

  def test_tags_not_set_identical
    @shirt.update_attributes tags: %w(sale)
    @shorts.update_attributes tags: %w(hot)

    @tagger.tag 'top'

    assert_equal %w(sale top), @shirt.tags
    assert_equal %w(hot top), @shorts.tags
  end
end

We can now start to build the ProductTagger model to make these tests green.


# app/models/product_tagger.rb

class ProductTagger
  attr_reader :product_ids

  def initialize(*product_ids)
    @product_ids = *product_ids
  end

  def tag(tag)
    product_ids.each do |id|
      product = Product.find id
      product.tags << tags
      product.save
    end
  end
end

Easy enough, right? But, I can already hear the screams. “You’re doing 2n queries!” You’re right, we do a find query, and an update query; FOR EACH RECORD. But, I didn’t say we were done…

Rails’ update_all Method

Our first helpful method is update_all provided by Rails. The full source is available here. The method is called on a relation, and can be passed a SQL string, an array, or a hash. For example, we could use the following to set the tags method.


# app/models/product_tagger.rb

def tag(tag)
  Product.find(product_ids).update_all tags: [tag]
end

This decreases our number of queries to one, but we now fail 2 of our tests. This will set the tags attribute, but overwrites the existing value. This is about as far as we can go with just Ruby/Rails. There is no way that I can find to do the work without iterating over the collection and updating one-by-one.

The Power of the Database

We’re already using some of the power of Postgres in the array type, let’s use even more of its features to handle these updates without so many queries.

The most helpful resource I found when solving this problem were, surprise, surprise, the Postgres docs.

The first Postgres function that jumped out at me was, array_append. This funtion takes 2 arguments.

  1. An array (the current tags field).
  2. An element to append to the array.

So, using our newfound function, let’s just append the tags attributes of each record.


# app/models/product_tagger.rb

def tag(tag)
  Product.find(product_ids).update_all "tags = array_append(tags, #{tag})"
end

Simple enough right? We are now only failing one test, the test that says that tags should not be duplicated. We’re able to continually append elements to the tags array.

The next Postgres function that has been incredibly useful to learn is, unnest. This function takes one argument, an existing array, and returns SQL rows of the values of each element in the array.

We can now use one of the more common bits of SQL, SELECT DISTINCT. We can unnest an array into a set of rows, select only those records that are unique, and then cast them back into an array.

So, our final implementation is as follows:


# app/models/product_tagger.rb

def tag(tag)
  Product.find(product_ids).
    update_all "tags = ARRAY(SELECT DISTINCT UNNEST(array_append(tags, #{tag})))"
end

Conclusion

And that’s it!

We now have an object that can smartly tag multiple objects, all in one query.

We’ve been able to use 2 very powerful tools, Rails and Postgres, to perform a task. It was a great learning experience to dive through the docs, because I knew something should be possible, I just didn’t know how.

Handling Money in Rails – Part III

This is Part III in a series of posts addressing the problems and solutions that I have encountered while building an eCommerce application. Part I and Part II are available here.

For everyone who’s been following along, we’ve already built a Payment class and explored various ways of “monetizing” the class. We’ve built our own accessor methods, and used ActiveRecord::Base‘s composed_of method.

We’re now at the point where we may have multiple classes that will all be dealing with money, and we’re looking for something a little less cumbersome.

Rails Magic

I still distinctly remember the first time I used Rails and I saw how easy it was to define relationships between models. The first time I used has_many, I was hooked; even if I didn’t know how it worked.

So, today, we’re going to define our own method monetize that will do all of the work that we did in our first two posts.

Our end goal will look like this:


# app/models/payment.rb

class Payment < ActiveRecord::Base
  monetize :amount
end

Very succinct, but I also believe very expressive. I think that most people dipping into this code will understand that the amount method will return some sort of “monetized” result. This is much cleaner than defining our own getters and setters, or even using the composed_of method which can be a little confusing.

They’re Just Class Methods!

The day that I realized that has_many, belongs_to, and acts_as_list were just class methods, was when I realized the power of being able to define my own.

So, let’s go ahead and define self.monetize that we can call on our payment class. As you’ll quickly see, I’ve made the decision to define my “monetized” methods using our original approach of having getters and setters. I could just as easily dynamically added composed_of blocks of code, but I’d like this to work on any Ruby object, not just those that inherit from ActiveRecord.


# app/models/payment.rb

monetize :amount

def self.monetize(field)
  define_method field do
    amount = send "#{field}_cents"
    currency = send "#{field}_currency"

    Money.new amount, currency
  end

  define_method "#{field}=" do |value|
    raise ArgumentError unless value.respond_to? :to_money
    money = value.to_money
    send "#{field}_cents=", money.cents
    send "#{field}_currency=", money.currency_as_string
    money
  end
end

This object should still pass all of our original tests with no issues.

How is This Any Different?!

I know, I know, this really didn’t do anything for us. We’re still defining the getters and setters on the class, but now we just have a new method that wraps it. But, we can now extract this method into its own Module and use it anywhere!

So, let’s start by pulling this block of code into its own file in our lib directory.


# lib/monetizable.rb

module Monetizeable
  def self.included(base)
    base.send :extend, ClassMethods
  end

  module ClassMethods
    def monetize(field)
      define_method field do
        amount = send "#{field}_cents"
        currency = send "#{field}_currency"

        Money.new amount, currency
      end

      define_method "#{field}=" do |value|
        raise ArgumentError unless value.respond_to? :to_money
        money = value.to_money
        send "#{field}_cents=", money.cents
        send "#{field}_currency=", money.currency_as_string
        money
      end
    end
  end
end


# app/models/payment.rb

class Payment < ActiveRecord::Base
  include Monetizable
  monetize :amount
end

Wrapping Up

We’ve now created a module that can be used in any class in our application that can “monetize” mutliple attributes. You could also hook into ActiveRecord with an initializer to include the module automatically.

If you’re curious, I’ve also put this into a small gem. Be sure to check out the lint test to easily ensure that your classes are properly monetized.

This is officially the end of our series about handling money in Rails. I’ll continue to post on a regular basis about all of the things I’ve learned along my journey.