What I learned in two years at Getaround

I joined Getaround, which was still named Drivy back then, two years ago. My previous and most extended professional experience had an internal organization that did not allow me to code full time, so many of my technical projects were actually side projects working alone. Although I could choose my topics and constraints, working alone […]

I joined Getaround, which was still named Drivy back then, two years ago. My previous and most extended professional experience had an internal organization that did not allow me to code full time, so many of my technical projects were actually side projects working alone.

Although I could choose my topics and constraints, working alone does not always help to learn good practices and tips that make a developer efficient and aware of the different technical challenges.

A few weeks ago, I took the time to understand how working in a (brilliant) team made me progress so much, not only as a Ruby developer but as a “Tech”. Here are a few topics that I learned or progressed on in the past two years.

Ruby and Rails-related APIs

tap and then

I love Kernel#tap because it lets me compose objects with conditions without having to add multiple conditional blocks.

def elements
  arr = [first_element]
  arr << second_element if available?
  arr
end

# versus

def elements
  [first_element]
    .tap { |arr| arr << second_element if available? }
end

We can argue that this is neither necessary nor more performant, but most of us love Ruby because it makes us write concise and straightforward code. I am feeling more comfortable with less procedural code.

On the same topic, we also have Kernel#then which is comparable to tap but returns the result of the block. This is very helpful when building conditional requests without having to add big if blocks:

Order
  .where(country: :fr)
  .then { |relation| completed? ? relation.order(:completed_at) : relation }
  .last

then is just an alias for yield_self introduced in Ruby 2.5.

Active Record Transactions

Transactions enforce the integrity of the database by wrapping several SQL statements into one atomic action. I find them not only useful but sometimes even essential. In some cases, you have to ensure several changes were made successfully or to cancel them all.

The following example is quite explicit about how convinient transactions are:

ActiveRecord::Base.transaction do
  @order.cancel!
  @car.available!
  create_ticket(@car)
end

If ticket creation were to fail, I am sure not to leave the car available or the order canceled, since the transaction will roll back.

Design Patterns

Command pattern

A lot of articles exist about this topic on the web. We even wrote about it a while ago in our Code Simplicity series by Nicolas.

The command pattern is a great way to extract business logic from controllers or even models, stay tied to the Single-responsibility principle and share a common API for service objects.

Although as a pattern, this one must be used carefully because it cannot resolve every situation. Jason Swett has even an interesting point of view about using this pattern in the Rails community.

Form objects pattern

A form object is a simple class that handles logic from a form submission. This class can be associated with the command pattern to share a common API with multiple form objects in your app.

Not only does this pattern allow you to extract business code from the controller and make it more testable, but it is also a great way to have different validations for the same model. You cannot always share a common form or even common validations depending on your action, for instance, when handling a user account. The rules applied to form parameters in a user registration are not the same as an account update.

Take the terms of service for instance. You probably want to ensure a terms_of_service parameter is present and true when signing up, but this requirement is unnecessary for a user updating her account. Having multiple form objects depending on the feature is a great help for this.

Jean also wrote about it on our blog a few years ago.

Facade pattern

The Facade pattern is proper when (but not only) decoupling business code from third-party code.

Let’s take the example of a third-party web API prividing its own gem.

car = GreatApi::Car.fetch(id)

Using it directly sometimes can be less maintainable as you don’t own its public API and are vulnerable to changes. What if you need to update this great_api gem for security reasons, but the gem changed its Car::fetch method to Vehicle::get? You would need to change every occurrence of GreatApi::Car::fetch in your business code to handle this breaking change.

Building a gateway around the gem ensures you to own it and encapsulate third-party code in one single place.

class Getaround::Gateway::GreatApi
  class << self
    def get_car(id)
      GreatApi::Car.fetch(id)
    end
  end
end

car = Getaround::Gateway::GreatApi.get_car(id)

Tell, don’t ask

I try to remember the “Tell, don’t ask” principle when designing a brand new object to keep in mind what OOP is about: designing objects being able to interact. Therefore an object should describe itself its behavior rather than having a program asking it what it is composed of to predict its behavior.

This example of Thoughbot’s blog is quite explicit:

# Instead of asking the system monitor for temperature
# in order to trigger an alarm

def check_for_overheating(system_monitor)
  if system_monitor.temperature > 100
    system_monitor.sound_alarms
  end
end

# Let it internally handle the rules (attributes)
# and trigger the alarm (behaviour)

class SystemMonitor
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

system_monitor.check_for_overheating

Gems

Delayed deprecations

Sometimes we want to ship fast, but we still want to ship well. There are some cases where we want to release code that is meant to be temporary, or to be reminded to monitor some behaviors once a feature has been live for a few weeks.

Temporary code is often associated with forgotten code and then technical dept, if not bugs. But delayed deprecations are a great way to keep a codebase clean month after month.

DelayedDeprecation.new("Only for April fools day",
  reconsider_after: Date.new(2021, 4, 1),
  owner: "Alice",
)

Deprecations trigger notifications to their owners for both Ruby and JavaScript code. This can be useful to remind you to clean up a piece of code.

I enjoy using them because it helps me staying efficient while maintaining a clean codebase.

Feature flipper

Another game-changer for our velocity and confidence when shipping new features is the feature flipper. It enables us to make some features available for a percentage of users or a percentage of time.

This is particularly useful to test changes and measure their impact without risking changing habits for all our users. If we need to urgently cancel a feature – because Murphy’s Law is always lurking – we can do so without deploying an urgent fix to hide it.

It doesn’t prevent us from being cautious and striving to ship high-quality tested code. Still, we are far more confident in ourselves when we know we can quickly handle unpredicted behaviors.

Timecop

Quite often, we have to test behaviors in the past or the future. Sometimes we also want to test a feature without time variation, for example, to avoid test flakiness.

Timecop is the perfect tool for this with a simple and comprehensive API.

context "when rental is ended for a month" do
  it "still respects some fundamentals rules" do
    Timecop.freeze(1.month.from_now) do
      # ...
    end
  end
end

context "when booking failed 5 minutes ago" do
  before do
    Timecop.travel(5.minutes.ago) do
      booking.failure!
    end
  end

  it "created a notification" do
    # ...
  end
end

Good practices

Zero downtime migrations

Zero downtime migrations are a pretty common thing, but to be honest, I never had not the chance to work with this process before working at Getaround.

The rule of thumb is to ensure any migration being deployed is compatible with the code already running. For instance, you cannot deploy at once a migration renaming a table’s column and the code handling the new column name. There is a very high chance that someone will run the app while the migrations haven’t been run yet and the column name doesn’t refer to anything yet.

Simple caution must be taken with multiple deployments such as:

  1. Add a new column with the new name
  2. Ensure both old and new columns are equally filled
  3. Back-fill data from the old column to the new one
  4. Stop using and referring to the old column
  5. Ignore the old column
  6. Remove the old column

Specs to cover future changes

Writing decoupled and reusable code is great. Ensuring this code will be properly used by others is even better. When I write code that can be shared or with variable data, I try to make sure nobody can add use cases that would break my code.

Let’s take an example, I am adding a state attribute to Car and I want to localize each state.

en:
  activerecord:
    attributes:
      car:
        states:
          active: Active
          deactivated: Not available

This is great, now I am able to use I18n.t("activerecord.attributes.car.states.#{car.state}").

But what if two months later, another developer adds a pending state? It would break when somebody runs my code with a pending car, and I would only be warned about it when facing the bug itself.

To avoid this situation, when adding the new state attribute, I also add specs to ensure all states have an associated translation:

Car.states.each do |state, _|
  it "has an associated translation for #{state}" do
    expect(I18n.t("activerecord.attributes.car.states.#{state}")).not_to raise_error
  end
end

RSpec mocking

Some people love it, some people don’t; in either case, we have to admit RSpec mocking is quite powerful. My perspective on the subject is to avoid mocking in feature specs as we want to stay as close as possible to a real-world example. When I have too much mocking to do to test a method, I probably need to think about the method/class dependencies.

Anyway, if you decide to use mocking, RSpec is a sweet candy. It helps you write difficult test cases with complex dependencies without instantiating tons of real objects and data.

Let’s take an example where I need an object to return a particular value. But this method has complex rules to return this value:

def pro?
  validated? && bank_account.allowed? && country.enables_pro? && electric_vehicles.any? # && ...
end

It is expensive to write and compute all requirements for this method, and I may even want it to return different results. I don’t want to be validating this pro? method neither; this is not the purpose of my test. With mocking, I can allow this object to receive this method and return the value I need for my test, instead of the value it would have returned with its default state.

context "when owner is pro" do
  before do
    allow(owner).to receive(:pro?).and_return(true)
  end

  # ...
end

Once again, let’s not forget that this powerful tool must be used cautiously; if an object is hard to unit test, maybe it is too much coupled with another object, or the abstraction is wrong.

Auto document a base class with NotImplementedError

Finally, ensuring the next developer, who could be yourself 6 months from now, is using your base class properly. When creating a base class meant for inheritance, you may need that its children implement a method.

Using the NotImplementedError standard error is a good way to ensure the method is implemented and to document it as necessary for child instances.

class Car
  def engine
    raise NotImplementedError, "#engine must be implemented on Car's children instances"
  end
end

class UrbanCar < Car
end

UrbanCar.new.engine
# => NotImplementedError is raised

Conclusion

I could add many more topics, even some simpler ones. With these tips, I am more confident now than I was 2 years ago, and I am looking forward to learning more in the years to come.

Of course, some may seem common sense to you, or even not necessary. I may also have forgotten good practices that look like a must have to you. If so, please feel free to reach us and debate.

Source: Drivy