As your business grows bigger, you just can’t stop adding new models/controllers to your original rails application – resulting in a messy, unmaintainable and difficult to deploy monolithic application. Its time to refactor. This talk will share our experience, results and best practices in splitting a single rails “application-system” into 30 independently maintainable yet interconnected applications.
After two and a half years of development (starting in pre-Rails 1.0 days!), our live-trainer English learning system now supported multiple roles (learner/trainer/trainer supervisor/sales/materials creation/support/etc) and an exhaustive list of features to support our complex business processes. We set ourselves a year-long goal of splitting this monolithic system into small cooperating applications that could be developed independently by individual developers. At the same time, we could not lose the usability cohesiveness and data-interdependence that defined the power of our system.
Through numerous iterations, many mistakes and a bit of pure-luck we developed an optimized process for the refactor and best practices for making 30 independent rails apps behave as one. The results: lower development time, greater stability and scalability and much higher developer happiness.
We’ll talk about specific code, measurements, pitfalls, plugins, process and best practices to answer questions such as:
How to know where to split single applications into many. How to measure the result.
How the applications should interact with each other. How to reduce administration and DRY configuration applications.
How to share data among applications.
How to DRY for common logic.
How to make a consistent user experience.
How to interact with non-Ruby technology; in our case Erlang, FreeSWITCH (VoIP) and Flex
7. The entire web application/system/platform runs as one single rails application (We are talking about really large systems. Multiple different types of clients/functions)
17. Consistent UI Shared CSS/JS/Styleguide Common Helpers in Shared Gem Safely try new things
18. interface All applications use the same base CSS/JS Keep all the application the same style <%=idp_include_js_css%> # => <script src ="/assets/javascripts/frame.js" type="text/javascript"></script> <link href="/assets/stylesheets/frame.css" media="screen" rel="stylesheet" type="text/css" />
23. interface Common Helpers: List table (cont) <%= idp_table_for(@history_records,:sortable=>true,:customize => "history_records") do |item, col| col.add:id, link_to(item.id, admin_history_record_path(item)),:order=>:id col.build:duration, :waiting_time, :review_time col.add:scenario, item.scenario_title, :order => :scenario_title col.add:mark_spot_num end %>
24. interface Development Lifecycle Implement new View code/plugin in a second application Abstract intoplugin using existing “idp” helpers Put it into main view gem
29. Purchase App data Requirement: List course packages for user to select to purchase but The course package data is stored in the “course” application
48. Features of User Service user Registration/login Profile management Role Based Access Control
49. Access Control user Each Controller is one Node * Posted to user service when app starts
50. Access Control user before_filter:check_access_right defcheck_access_right unlessxml_request?orinner_request? access_deniedunlesshas_page_right?(params[:controller]) end end * Design your apps so access control can be by controller!
53. Step 1: User Auth user config/initializers/idp_initializer.rb ActionController::Base.session_store = :active_record_store ActiveRecord::SessionStore::Session.acts_as_remote:user, :readonly=> false
54. Step 2: Access Control user Tell core its controllers structure CoreService. reset_rights defself.reset_rights data = load_controller_structure self.post(:reset_rights, :data =>data) end
55. Step 2: Access Control user before_filter:check_access_right defcheck_access_right unlessxml_request?orinner_request? access_deniedunlesshas_page_right?(params[:controller]) end end
56. Step 2: Access Control user has_page_right? Readonly dbconn again
57. Step 2: Access Control user class IdpRoleRight < ActiveRecord::Base acts_as_readonly:user, :table_name=> "role_rights" end def has_page_right?(page) roles = current_user.roles roles_of_page = IdpRoleRight.all(:conditions => ["path = ?", page]).map(&:role_id) (roles - (roles - roles_of_page)).size > 0 end
61. File class Article < ActiveRecord::Base has_files end Specify Class that Has Files Upload File in Background to FileService Idp_file_form Store with app_name, model_name, model_id Use readonly magic to easily display @article.files.first.url
62. service Comet classChatRoom < ActiveRecord::Base acts_as_realtime end <%= realtime_for(@chat_room, current_user.login) %> <%= realtime_data(dom_id, :add, :top) %> @chat_room.realtime_channel.broadcast(“hi world", current_user.login)
63. service mail Mail services MailService.send(“test@idapted.com”, :welcome, :user => “test”)
64. Host all in one domain Load each rails app into a subdir, we use Unicorn unicorn_rails --path /user unicorn_rails --path /studycenter unicorn_rails --path /scenario
65. Host all in one domain use Nginx as a reverse proxylocation /user { proxy_pass http://rails_app_user; } location /studycenter { proxy_pass http://rails_app_studycenter; }
66. Host all in one domain All you see is a uniform URL www.eqenglish.com/user www.eqenglish.com/studycenter www.eqenglish.com/scenario
76. Measurement Critical and core task of single app should not call services of others. One doesn’t need to know much about others’ business to do one task (or develop). Independent Stories