Article voting schema
Rails just isn't taking chances, so when you call vote.article it's performing a new DB query, and thus creating a new instance rather than grabbing a cache. And that's a good thing! You might run into trouble otherwise, since you call save. If it was the same Article instance everywhere, you'd suddenly be calling save on it from a non-obvious point in the code.

I.e. pretend that Rails does reuse the same Article instance, and that instance has other unsaved changes. Perhaps these changes aren't valid, and in that case, the save call in ArticleVote would fail although you're only trying to change the article's score. It would also update the article's updated_at timestamp, although that may not be relevant; an article receiving or losing a vote doesn't really mean that the article itself was updated.

So, for one I'd recommend using article.update_column(:score, article.score - value) in you before_destroy method. And, yes, you'll still need to call reload elsewhere.

However, the underlying issue is that you're modifying an article "from the outside". I'd recommend a different approach altogether, with no articles.score column in the database, and using Rails' caching API instead:

class Article
  has_many :votes, class_name: "ArticleVotes"

  # score is not part of the DB schema;
  # it's computed and cached as necessary
  def score
    # get or update the cached score
    Rails.cache.fetch(score_cache_key) { votes.sum(:value) }
  end

  def flush_score_cache
    Rails.cache.delete(score_cache_key)
  end

  # let's not use or override the built-in
  # #cache_key method for this, since that
  # relies on the updated_at timestamp.
  def score_cache_key
    "article/#{id}/score" # or something similar with the ID in it
  end
end


(In the above, you may want to handle the possibility of the record being new, and thus having a nil id.)

And in ArticleVote:

class ArticleVote
  after_commit :flush_article_score

private

  def flush_article_score
    article.flush_score_cache
  end
end


Now whenever a vote is added, removed or updated, the article's score cache will be busted, forcing a article.score to do a proper recount if/when it's called.

This also has the advantage that the article's score remains consistent with the actual votes: It's computed entirely from what's in the database.  Alternatively, you can simply increment/decrement the cached value instead of flushing it, and still avoid having a database column.

Of course, there are some drawbacks too. Since it is a cache the usual disclaimers apply (you have to be diligent about busting it when necessary, or you'll hit a stale cache, etc..). And if you've got heavy voting traffic you might end up busting the cache so often it just always does the SUM() query. In that case, you could still go with your current solution of having a database column that you increment and decrement, but I might still want to stick that in a different table to avoid the aforementioned issues with implicitly calling save on an article (or, like now, overwriting an updated score with a older one if you forget to reload). You could also consider a more "raw SQL" approach of actually incrementing or decrementing rather than writing the value directly, i.e. UPDATE ... SET score=score + 1 WHERE .... That would avoid race conditions. Optimally, you might do a bit of both: A database column holding the canonical score of an article on disk, and a memory cache you use most of the time for reads, so you only have to hit the database for writes.