Make the BIG Change

(One SMALL Change at a Time)

Chris Geihsler

@seejee

Hi, I'm Chris

SOFTWARE is CRUFTY 

BIG changes are HARD 

CHANGE it SAFELY 

ONE

COOL

TRICK

TELL a STORY 

TAKE something AWAY 

3 million STUDENTS

ROSTERS change

SIS

SIS

SIS

SIS

SIS

SIS

SIS

SIS

SIS

SIS

2013

+

PROBLEM?

120,000

SUPPORT:

3 DEVELOPER-YEARS

GOAL

Drastically reduce Clever-related support burden.

DESIGN cruft

TAKEAWAY

Protect your data from 3rd party integrations.

SOLUTION

Know what a Clever sync will change without making any changes.

Summary of APPLIED Changes

¯\_(ツ)_/¯

Summary of PROPOSED Changes

SYNC

CODE cruft

GOAL

Lay the foundation for future integrations.

PHASE 0:

High Level Plan

SYNC

ACTIONS

Student::Create

Student::ReassignSchool

Classroom::AddStudentToClassroom

Error::ValidationError

Teacher::Update

School::Deactivate

Error::WontAddStudentToClassroom

Error::CantMoveSchool

AcrossCustomers

PLAN

Generate a list of actions that can be  invoked later.

INCREMENTAL

or

REWRITE?

REQUIREMENTS?

BIGGER CHANGES

BIGGER BREAKS

TAKEAWAY

Prefer incremental approach unless you have a high tolerance for risk.

PROCESS?

   11 TEAM MEMBERS

23.6 PRs/WEEK

  2.1 PRs/WEEK/PERSON

TAKEAWAY

Make it easy to test and deploy small, isolated changes.

5 PHASES

    ~5 PRs/PHASE

TAKEAWAY

Planning is more important than the plan itself.

TAKEAWAY

Big changes are easier when your stakeholders buy-in.

PHASE 1:

Extract and Isolate

  class Student
    # 1100 lines

    def update_from_clever(clever_student)
      # 100 lines
    end
  end

  module CleverUtils
    # 500 lines

    def create_student(clever_student)
      # 50 lines
    end
  end
  class Sync::Student

    def sync(clever_student)
      student = find_student(clever_student)

      if student.nil?
        create(student, clever_student)
      else
        update(student, clever_student)
      end
    end

    # more methods
  end
  class Student
    #
    # a little smaller
    #
  end

  module CleverUtils
    #
    # a little smaller
    #
  end

TAKEAWAY

Complexity becomes apparent when code is in small, isolated pieces.

TAKEAWAY

You can't fix everything at once.

PHASE 1:

6 Pull Requests

~ 4 weeks

TANGENT:

TESTING

80% INTEGRATION

20% UNIT

SLOWED BY SLOW TESTS

TAKEAWAY

Refactor your tests as aggressively as your production code.

TAKEAWAY

Delete tests that provide minimal value.

PHASE 2:

Introduce Actions

  class Sync::Classroom
    def update(classroom, clever_classroom)
      # do some update stuff

      if !classroom.students.include?(student)
        classroom.add_student(student)
      end
    end
  end
  class Sync::Classroom
    def update(classroom, clever_classroom)
      actions = []

      # generate some actions
      
      actions
    end
  end
  
  # new
  actions <<
    Actions::AddStudentToClassroom.new(
      student_id:   student.id
      classroom_id: classroom.id
    )
 
  
  # original
  classroom.add_student(student)
  class Actions::AddStudentToClassroom
    attr_reader :student_id, :classroom_id

    def invoke
      student   = Student.find(student_id)
      classroom = Classroom.find(classroom_id)

      classroom.add_student(student)
    end
  end
  # original
  def sync_students
    clever_students.each do |clever_student|
      Sync::Student.new.sync(clever_student)
    end
  end
  # new
  def sync_students
    actions = 
      clever_students.flat_map { |clever_student|
        Sync::Student.new.sync(clever_student)
      }

    actions.each do |action|
      action.invoke
    end
  end

TAKEAWAY

Decoupling decisions from actions will improve system flexibility.

PHASE 2:

7 Pull Requests

~ 3 weeks

PHASE 3:

Serialize Actions

id type payload invoked_at
1 action #JSON 2015-05-05
2 action #JSON NULL
3 error #JSON NULL
  class Actions::AddStudentToClassroom
    attr_reader :student_id, :classroom_id

    def to_json
      {
        _class:       'AddStudentToClassroom',
        student_id:   student_id,
        classroom_id: classroom_id 
      }
    end
  end

  def sync_students
    actions = 
      clever_students.map { |clever_student|
        Sync::Student.new.sync(clever_student)
      }

    save_actions(actions)

    invoke_pending_actions
  end

TAKEAWAY

Interim states won't be ideal. That's OK.

PHASE 3:

3 Pull Requests

~ 2 weeks

PHASE 4:

Defer Actions

  class Synchronizer
    def sync
      save_actions(sync_schools)
      invoke_pending_actions

      save_actions(sync_teachers)
      invoke_pending_actions

      save_actions(sync_students)
      invoke_pending_actions
      
      save_actions(sync_classrooms)
      invoke_pending_actions
    end
  end
  class Synchronizer
    def sync
      save_actions(sync_schools)
      save_actions(sync_teachers)
      save_actions(sync_students)
      save_actions(sync_classrooms)
    end

    def invoke
      invoke_pending_actions
    end
  end

Student::Create

PENDING ACTIONS

Classroom::Create

  
    actions <<
      Actions::AddStudentToClassroom.new(
        student_id:   student.id
        classroom_id: classroom.id
      )

PROBLEM

How do we refer to things that don't exist yet?

  class EntityReference
    def to_json
      [
        { field: 'id'       , value: XXXX},
        { field: 'clever_id', value: YYYY}
      ]
    end

    def find
    end

    def ==(other)
    end
  end
  # new
  if !current_student_refs.include?(student_ref)
    actions <<
      Actions::AddStudentToClassroom.new(
        student_ref:   student_ref
        classroom_ref: classroom_ref
      )
  end
  # original
  if !classroom.students.include?(student)
    actions <<
      Actions::AddStudentToClassroom.new(
        student_id:   student.id
        classroom_id: classroom.id
      )
  end
  class Actions::AddStudentToClassroom
    attr_reader :student_ref, :classroom_ref

    def invoke
      student   = student_ref.find
      classroom = classroom_ref.find

      classroom.add_student(student)
    end
  end

PROBLEM

How do we account for pending changes?


    if student.school == classroom.school
      Actions::AddStudentToClassroom.new(
        student_ref:   student_ref
        classroom_ref: classroom_ref
      )
    else
      Errors::WontAssignClassroom.new(
        student_ref:   student_ref
        classroom_ref: classroom_ref
      )        
    end

Student::Update

Student::ReassignSchool

PENDING ACTIONS

Student

TTM MODEL


    if student.school == classroom.school
      Actions::AddStudentToClassroom.new(
        student_ref:   student_ref
        classroom_ref: classroom_ref
      )
    else
      Errors::WontAssignClassroom.new(
        student_ref:   student_ref
        classroom_ref: classroom_ref
      )        
    end

Student::Update

Student::ReassignSchool

Student

+

+

=

Pending Student State

  
  class Data::Student
    :attributes,
    :school_ref,
    :classroom_refs
  end
  
  def pending_state_for(student_ref)
    initial_state = 
      if student_ref.exists?
        extract_state(student_ref.find)
      else
        empty
      end

     pending_actions_for(student_ref)
       .reduce(initial_state) { |state, action|
         action.apply(state)
       }
  end
  class Actions::Student::ReassignSchool
    attr_reader :student_ref, :school_ref

    def apply(current_state)
      current_state.merge(
        school_ref: school_ref
      )
    end

    def invoke
      student.reassign_school(school)
    end
  end

ONE

COOL

TRICK

state = f(existingModel, pendingActions)

newActions = f(cleverData, state)

summary = f(actions)

TAKEAWAY

Functional programming has broad applications.

TAKEAWAY

You don't need to use a functional language to program in a functional style.

PHASE 4:

7 Pull Requests

~ 4 weeks

PHASE 5:

Decouple from Clever

  
  class EntityReference
  end 

  class Data::Student
    :attributes,
    :school_ref,
    :classroom_refs
  end
class Sync::Clever::Student
  def find_entity(clever_student)
    EntityReference.new(clever_id: clever_student.id)
  end

  def extract(clever_student)
    Data::Student.new(
      attributes: {
                    first_name: clever_student[:first_name],
                    last_name:  clever_student[:last_name],
                  },
      school_ref: ...
    )
  end
end

PHASE 5:

2 Pull Requests

~ 1 week

PHASE 6:

Implement new CSV format

PHASE 6:

4 Pull Requests

~ 2 week

PRESENT:

Is it better?

5 BUGS

~24h TURNAROUND

PREVENTED ISSUES

SUPPORT:

FUTURE:

Implement

FUTURE:

Circuit Breaker

  
  if will_too_much_stuff_change?(pending_actions)
    reject_changes
  
    pause_syncing

    notify_support
  end

FUTURE:

Undo

TAKEAWAY

Treating mutations as data has lots of cool benefits.

TEAM

TAKEAWAY

Diverse teams make better products.

MORE Capable

Maintainable

Extensible

  • LIGHTWEIGHT PLAN

  • STAKEHOLDER BUY-IN

  • SMALL CHANGES

  • TESTS, TESTS, TESTS

  • FUNCTIONAL APPROACH

  • DIVERSE TEAM

TAKEAWAYS

CONQUER your CRUFT

TELL us a STORY 

THANKS!

Chris Geihsler

@seejee

make the big change

By Chris Geihsler

make the big change

  • 2,326