Building an Auction App in a Weekend

apps

It's 2014. Everything needs an app, and HubSpot's 5th Annual Charity Auction was no exception. Until this year, our silent auction worked the traditional way: people writing their bids on pieces of paper, walking away, and maybe coming back every once in a while to see if anyone had outbid them. Does this work? Sure. Would it be way cooler (and more fun) to let people bid with a mobile app? Definitely!

As it turns out, this isn't a new idea. A number of silent auction apps already exist, and our auction organizers looked into them. A recurring issue with each solution was that they cost hundreds to thousands of dollars. When you're raising sums in the tens-of-thousands range, that's a substantial loss. And despite the cost, most of the "apps" were either mobile websites, or native apps that simply wrapped the mobile website. Either way, the user experience suffered.

Eventually, the organizers asked HubSpot's mobile apps team (that's us!) if we would be interested in taking on the project. The only catch? The event was in three weeks, and we had to keep doing our actual jobs.

Reality Check

After a quick Friday morning conversation around what exactly the app would need to do, we decided to let them know by Monday whether finishing it in a couple of weeks was feasible.

A few signs pointed to yes.

First, the app's only users would be HubSpot employees and their plus-ones, who can be trusted to Use Good Judgment. Good Judgment™ includes things like not impersonating other people, or not bidding one trillion dollars on even the most priceless of items. It meant that we could trust users to provide their real email address, and not bother with usernames and passwords. And it saved having to write countless lines of code to defend against malicious users.

We also had the existing codebase from HubSpot's iOS and Android apps. The auction apps could borrow heavily from our existing apps, jumpstarting development.

hubspot

HubSpot app vs. the auction app. We're shameless.

Finally, we had just discovered Parse. Parse promised to take database persistence, back-end server logic and push notifications out of our hands, hiding those generally unpleasant parts of app development behind their pretty SDKs. Oh, and it wouldn't cost anything, unless our users managed to bid more than thirty times per second.

#JFDI

At HubSpot, we're big fans of actually doing things instead of sitting around talking about doing things. After our conversation, I blocked off the next few hours to investigate Parse and make sure it actually did what we thought it did.

parse

Spoiler: it does (screenshot from after the auction).

After spending a few minutes with the documentation, I signed up, created an Item table, and added a couple of Test Objects. Everything looked good, so I created a blank Android project, copied over all of the basics from the HubSpot app, and pulled in the Parse SDK. In theory, retrieving every item would take only a couple of lines of code:

ParseQuery<ParseObject> query = ParseQuery.getQuery("Item");
query.findInBackground(new FindCallback<ParseObject>() {
  public void done(List<ParseObject> items, ParseException e) {
    Log.i("AuctionTest", "Hello, " + items.size() + " items!");
Log.i("AuctionTest", "First item name: " + items.get(0).getString("name") + "."); } });
> Hello, 3 items!
> First item name: Test Object 1.

That was it! Parse handles the networking, the threads, and all of the other stuff that isn't writing auction apps. Neat. Just to make sure, I created a NewBid table and made sure that writing objects was as easy as reading them:

ParseObject newBid = new ParseObject("NewBid");
newBid.put("email", "jtsuji@hubspot.com");
newBid.put("amt", 50);
newBid.put("objectId", items.get(0).getString("objectId"));
newBid.saveInBackground();

It's almost too easy. Putting those two pieces together, we can retrieve all bids for a given item to find its current price:

ParseQuery<ParseObject> query = ParseQuery.getQuery("NewBid");
query.whereEqualTo("objectId", itemToPriceCheck.getString("objectId"));
query.orderByDescending("amt");
query.findInBackground(new FindCallback<ParseObject>() {
public void done(List<ParseObject> bids, ParseException e) {
Log.i("AuctionTest", "Highest bid: " + bids.get(0).getInt("amt") + ".");
}
});
> Highest bid: 55

In fewer than 15 lines of code, we now had the ability to retrieve all of the auction items, bid on them, and determine their current price. After putting those code snippets into functions and hooking them up to a rough UI, we had a fully-functional auction app prototype - only a few hours after our initial meeting. Convinced that the project was more than feasible, I decided to add as many features as possible over the rest of the weekend.

Push It

push4

Surely you won't stand for this.

Used correctly, push notifications are an excellent tool for engaging your users. Sassy push notifications that name names are even better. The instant notifications led people to bid more frequently:

Screen_Shot_2014-12-12_at_2.49.54_PM

Rapid one-upsmanship, brought to you by push notifications.

...and naming names resulted in a bidding war that saw two friends drive the "choose a new flavor of yogurt for the kitchen" item to a staggering $410 in the name of aimless rivalry (and charity!).

Clearly, push notifications worked  and they were easy to implement!

var query = new Parse.Query(Parse.Installation); 
query.containedIn("email", previousWinners.diff(item.get("currentWinners")));
Parse.Push.send({
where: query,
data: {
alert: itemBidMsg + "Surely you won't stand for this."
...
}
});

The logic is simple: If someone was previously winning and no longer is, send them a push. Parse handles GCM tokens, device IDs, and all of the other things needed to actually send the push. You may have noticed the push code is in JavaScript - that's because it's running on the Parse servers using something called Cloud Code. We'll get to that.

Scaling Up

Screen_Shot_2014-12-06_at_12.04.10_AM

Requests per second on the day of the auction.

The prototype app determined the current price of an item client-side, by retrieving all bids for that item and finding the highest one. Although that worked, it wouldn't have scaled well.

We had a total of 97 items. There are at least 700 HubSpot employees. That means if every employee scrolled through the list of items once, we would use:

97 * 700 = 67,900 requests.

It gets worse. To refresh the prices, we have to make the requests all over again. If everyone refreshed prices every ten minutes:

97 * 700 * (24 * 6) = way more than 30 requests/second.

One possible fix is keeping track of "highestBid" and "highestBidder" directly in each item, and having the apps edit those two values whenever the user places a higher bid. This eliminates the need for apps to query for bids (since the price is included in the item), dramatically reducing the number of requests.

Unfortunately, it's also a very dangerous way to do things. What if a request to change an item's highestBid to $350 arrives after a request to bid $375, due to network latency? The item's price would be updated to $350 and the $375 bid would be lost. What if the $375 bidder backs out, and we need to find out who bid $350? We'd have no way of knowing.

Cloud Code

Clearly, that's unacceptable. To solve this, we kept the bids table, so that we would have a record of each and every bid placed. We also kept the highestBid and highestBidder fields for items, so that clients never needed to comb through all of the bids to determine an item's price.

Parse's Cloud Code feature lets us add JavaScript functions that are called before and after anyone tries to add a new bid. These functions are run on Parse's servers, and the only limitation is that they must complete execution in three seconds. We used this capability to keep the highestBid and highestBidder fields up-to-date:

Parse.Cloud.beforeSave("NewBid", function(request, response) {
currentBid = request.object;
itemQuery = new Parse.Query("Item");
itemQuery.equalTo("objectId", request.object.get("item"));
itemQuery.first({
success: function(item) {
var date = Date.now();
if (date > item.get("closetime")) {
response.error("Bidding for this item has ended.");
return;
}

// Do the rest of the checks and update the item
...
});
Parse.Cloud.afterSave("NewBid", function(request, response) { // Push code });

The beforeSave function checks the previous bids for that item, making sure that the new bid is higher. It also checks that the item is still open for bidding, and a couple of other constraints. If any of these restrictions aren't met, it returns an error and the bid isn't saved. Otherwise, it updates the item's highestBid/highestBidder fields and saves the bid.

The afterSave function is called after a bid passes all of the tests and is successfully saved. Here, we send the push notifications to the item's previous winners. The apps refresh the list of items whenever a push is received, ensuring that item prices are up-to-date.

Real Data

Conversations with app testers revealed that although they loved bidding on Test Object 7, they preferred to bid on items with “actual value”. For this reason, we decided our next step should be importing all of the real items donated by HubSpot employees. Due to the last-minute development of the app, everyone had already added their donations to a page on our internal wiki.

Screen_Shot_2014-12-15_at_4.01.02_PM

This is easy for you to read because you are not a computer.

I copied the HTML code for that table into a convenient website that converts HTML to CSV. Then, I opened the resulting CSV in Excel, added a qty column to uniformly represent items with multiple winners, and cleaned up the data manually. Parse allows you to import data directly from a CSV, so the rest was easy.

Next year, we’ll have donors submit their items via a web form, which will add it to Parse automatically.

Finishing Up

monitoring2

Keeping an eye on things with an Angular app.

The core functionality was completed by Monday, three days after we learned about the project. We used the next few weeks to refine the bidding logic, improve the app UIs, and write a few tools like the Angular web app pictured above.

The silent auction was held at the same time as our holiday party, so hundreds of HubSpot employees were in the same room using the app, getting angry at the outbid notifications, running around trying to find whoever outbid them to convince them to give up, and generally having a great time with it. We raised $28,231 with nearly 1,400 bids placed, and none of that money spent went to supporting the nonexistent costs of the auction app. Parse is free, and our time was our contribution to the charity auction, so there were no costs.

Your Turn!

HubSpot loves open-source software, so we're open-sourcing this entire project under the name BidHub. We love hearing feedback, and especially love pull requests, so check out our GitHub repos:

Use the BidHub-CloudCode repo to set up Parse, clone BidHub-iOS and BidHub-Android to build the apps, and then put the BidHub-WebAdmin page somewhere to keep tabs on everything!

We have lots of ideas that we didn't have time to add. If you do one of these and submit a pull request, we will think you’re super awesome!

  • Full bid history in the app, so you can see who's currently winning and watch bidding wars in real-time.
  • $0.99 in-app purchase to customize the outbid notification message. "joshuatsuji bid $350: Want to fight about it? Lobby. 10 minutes. Be there."
  • Set a "high bid" and have the app automatically raise your bid up to that limit.
  • Notify donors when their item is bid on.
  • A more fully-featured web admin panel that lets you add/remove items. There's actually some commented-out code in the BidHub-WebAdmin repo where we started that!
  • "Sudden death" mode where bidding on an item doesn't close until one minute has passed since the final bid.

And if these kinds of projects sound like your thing: We're hiring! Get in touch.

 

Joshua Tsuji

Written by Joshua Tsuji

Comments

Subscribe for updates

New Call-to-action