The current setup for the feed reader application looks like the typical Rails deploy- ment mentioned earlier. There is an application server with background processing. The logic for sending out emails and performing updates of feeds occurs in these back- ground jobs. Everything is tied together fairly tightly. There are separate models for the emails and the feeds, but the logic of actually running these tasks is held within a single process.
The code in the social reader application looks like a regular Rails application. There are models, controllers, and views. When looking at services, most of what gets pulled into a service is the code and logic that lies at the model level. It helps to have in mind what the model schema and relationships for the data look like. The follow- ing sections outline the models in the application. Each one is annotated with its data- base schema using the annotate models gem (see http://github.com/ctran/ annotate_models/tree/master).
ptg
The User Model
The user model is the common starting place for most applications. Many of the other models are tied in some way to the user model, and the social feed reader application is no different. Every model can be reached through an association on the user model. When breaking up the data into services, some of these associations will have to cross service boundaries. The user model looks as follows:
class User < ActiveRecord::Base has_many :follows
has_many :followed_users, :through => :follows has_many :followings, :class_name => "Follow",
:foreign_key => :followed_user_id
has_many :followers, :through => :followings, :source => :user
has_many :comments has_many :votes
has_many :subscriptions
has_many :feeds, :through => :subscriptions has_many :activities,
:conditions => ["activities.following_user_id IS NULL"] has_many :followed_activities, :class_name => "Activity",
:foreign_key => :following_user_id end
# == Schema Information #
# Table name: users #
# id :integer not null, primary key # name :string(255)
# email :string(255) # bio :string(255) # created_at :datetime # updated_at :datetime
This example shows only the associations, which happen to add the finders that are needed for the application. There are a few has_many :through associations. Users
ptg can access their followers, who they are following, the comments they have made, the
feeds they are subscribed to, the activities they have performed in the system, and the activities of the users they are following.
The activities are contained in a single denormalized table. Another option would be to map activities through a polymorphic relationship, but such joins can be expensive. A denormalized structure makes retrieving the activities a quick operation. To get a sense of why the relationship for activities looks the way it does, let’s look at the activity model.
The Activity Model
The activity model is for keeping a record of user activity such as following another user, subscribing to a feed, or commenting or voting on an entry. It is a denormalized model, so there is some data duplication with the comment, follow, subscription, and vote models. The activity model looks as follows:
class Activity < ActiveRecord::Base belongs_to :user def self.write(event) create(event.attributes) event.user.followers.each do |user| create(event.attributes.merge(:following_user_id => user.id)) end end end
class CommentActivity < Activity end
class SubscriptionActivity < Activity belongs_to :feed
end
class VoteActivity < Activity end
ptg
class FollowActivity < Activity
belongs_to :followed_user, :class_name => "User" end
# == Schema Information #
# Table name: activities #
# id :integer not null, primary key # user_id :integer # type :string(255) # feed_id :integer # followed_user_id :integer # entry_id :integer # content :text # following_user_id :integer # created_at :datetime # updated_at :datetime
The activity model uses single-table inheritance (STI) to keep each type of activ- ity in the same table. The parent class defines a write method that should be called when a comment, subscription, vote, or follow is created. First, it writes an activity without followed_user_id, which is used in the user model to find the activities that the specific user performed. Then write creates a new activity for each of the user’s
followers. This is another instance of data duplication, but it cuts down on the num- ber of joins that must be performed to pull the activity for all the users an individual is following.
The Follow Model
The follow model is the join model that specifies which users are following the oth- ers. It looks like this:
class Follow < ActiveRecord::Base belongs_to :user
ptg
after_create {|record| FollowActivity.write(record)} end
# == Schema Information #
# Table name: follows #
# id :integer not null, primary key # user_id :integer
# followed_user_id :integer # created_at :datetime # updated_at :datetime
The follow model contains only the two user IDs of the follower and followee. The logic for creating activities after create is contained in the model.
The Feed Model
The feed model contains the basic data for RSS or Atom feeds in the system. Here’s how it looks:
class Feed < ActiveRecord::Base has_many :entries
has_many :subscriptions
has_many :users, :through => :subscriptions end
# == Schema Information #
# Table name: feeds #
# id :integer not null, primary key # title :string(255)
# url :string(255) # feed_url :string(255) # created_at :datetime # updated_at :datetime
The relationships for the feed model show that it has many entries (the specific blog posts) and many users through the subscriptions.
ptg
The Subscription Model
The subscription model maps users to the feeds they are subscribed to. It looks like this:
class Subscription < ActiveRecord::Base belongs_to :user
belongs_to :feed
after_create {|record| SubscriptionActivity.write(record)} end
# == Schema Information #
# Table name: subscriptions #
# id :integer not null, primary key # user_id :integer
# feed_id :integer # created_at :datetime # updated_at :datetime
The subscription model is simple, with only a relationship with the user and the feed. The logic to create subscription activities is in the after create block.
The Entry Model
The entry model contains all the information for a specific article or blog post from a feed. Here’s how it looks:
class Entry < ActiveRecord::Base belongs_to :feed
has_many :comments end
# == Schema Information #
# Table name: entries #
ptg
# id :integer not null, primary key # feed_id :integer # title :string(255) # url :string(255) # content :text # published_date :datetime # up_votes_count :integer # down_votes_count :integer # comments_count :integer # created_at :datetime # updated_at :datetime
The entry model has relationships to the feed that it belongs to and the comments associated with it. There are also counters for the number of up votes, down votes, and comments. It could also contain a has-many relationship to those votes, but from the entry’s point of view, the only important thing for the application to keep track of is the count of vote types.
The Vote Model
The vote model uses STI to define the two different types of votes, the up and down votes:
class Vote < ActiveRecord::Base belongs_to :user
end
class UpVote < Vote
belongs_to :entry, :counter_cache => true
after_create {|record| VoteActivity.write(record)} end
class DownVote < Vote
belongs_to :entry, :counter_cache => true end
ptg
# == Schema Information #
# Table name: votes #
# id :integer not null, primary key # user_id :integer # entry_id :integer # type :string(255) # rating :integer # created_at :datetime # updated_at :datetime
The parent class vote defines the relationship to user that both the up and down
vote classes require. The up and down votes both define their relationships to the entry because of the automatic counter cache. This gets incremented in up_votes_count or down_votes_count on the entry object. Finally, only the up vote writes activity. This
is because the users probably don’t want to see entries that the people they are follow- ing thought were bad.
The Comment Model
The comment model is very basic. It holds only the text of a comment from a user and the associated entry:
class Comment < ActiveRecord::Base belongs_to :user
belongs_to :entry, :counter_cache => true
after_create {|record| CommentActivity.write(record)} end
# == Schema Information #
# Table name: comments #
# id :integer not null, primary key # user_id :integer
ptg
# entry_id :integer # content :text # created_at :datetime # updated_at :datetime
The relationships to the user and entry are here in the comment model. Also, a counter cache is kept up on the entry to store the number of comments. Finally, after creation of a new comment, the activity is written.