Slide 1

Slide 1 text

In Limbo Managing Transitional States

Slide 2

Slide 2 text

Jeremy Smith @jeremysmithco

Slide 3

Slide 3 text

Limbo What I mean by

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Limbo an intermediate or transitional place or state a state of uncertainty

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

In Limbo Lundy, Fastnet, Irish Sea Ive got a message I cant read Im lost at sea Dont bother me Ive lost my way

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

My Meaning The incremental process of making a complex change to a software system in use.

Slide 12

Slide 12 text

My Meaning The incremental process of making a complex change to a software system . in use

Slide 13

Slide 13 text

My Meaning The incremental process of making a change to a software system in use. complex

Slide 14

Slide 14 text

My Meaning The incremental process of making a complex change to a in use. software system

Slide 15

Slide 15 text

Some examples

Slide 16

Slide 16 text

Dependency Change gem acts_as_paranoid client .find( ) client.destroy client.persisted? client.deleted_at client .find( ) client.really_destroy! client.persisted? client .all client .with_deleted "paranoia" class < end >> = >> >> >> >> = >> >> >> = >> = Client ApplicationRecord Client 1 Client 2 Client Client # => true # => 2025-04-03 4:26:02 PM # => false # Only clients not soft-deleted # All clients

Slide 17

Slide 17 text

Dependency Change class < end class < end >> = >> >> >> >> acts_as_paranoid has_many , belongs_to client .find( ) client.emails.size client.destroy client.persisted? client.reload.emails.size Client ApplicationRecord Email ApplicationRecord :emails dependent: :destroy :client Client 1 # => 74 # => true # => 0

Slide 18

Slide 18 text

Dependency Change gem :: .discard_column client .find( ) client.discard client.persisted? client.deleted_at client .find( ) client.destroy client.persisted? client .all client .kept "discard" class < include = end >> = >> >> >> >> = >> >> >> = >> = Client ApplicationRecord Discard Model self :deleted_at Client 1 Client 2 Client Client # => true # => 2025-04-03 4:26:02 PM # => false # All clients # Only clients not soft-deleted

Slide 19

Slide 19 text

Schema Change class < end enum , { , , } User ApplicationRecord :role regular: manager: admin: "regular" "manager" "admin"

Slide 20

Slide 20 text

Schema Change class < end class < end class < end class < end has_many belongs_to belongs_to has_many belongs_to belongs_to validates , , User ApplicationRecord Membership ApplicationRecord MembershipRole ApplicationRecord Role ApplicationRecord :memberships :user :team :membership_roles :membership :role :name presence: true uniqueness: true

Slide 21

Slide 21 text

Data Process Change class < def end private def new end end has_one_attached update!( calculate_unique_word_count) . (plain_text, ).calculate Document ApplicationRecord update_metadata! calculate_unique_word_count :file unique_word_count: WordCountCalculator unique: true

Slide 22

Slide 22 text

Data Process Change class def = = end def = ? end private attr_reader end (text, ) @text text.to_s @unique unique words text.split( ) unique words.uniq.count : words.count , WordCountCalculator initialize calculate unique: false :text :unique / / \s+

Slide 23

Slide 23 text

Why Not All-At-Once

Slide 24

Slide 24 text

Why Not All-At-Once Integration

Slide 25

Slide 25 text

Why Not All-At-Once Integration Release

Slide 26

Slide 26 text

Why Not All-At-Once Integration Release Reversion

Slide 27

Slide 27 text

Why Not All-At-Once Integration Release Reversion Cognitive Load

Slide 28

Slide 28 text

Why Not All-At-Once Integration Release Reversion Cognitive Load Stress

Slide 29

Slide 29 text

Story Time

Slide 30

Slide 30 text

CSS Frameworks Migrating

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

Constraints

Slide 33

Slide 33 text

Constraints No Blocking Other Work

Slide 34

Slide 34 text

Constraints No Blocking Other Work No Long-Running Branches

Slide 35

Slide 35 text

Constraints No Blocking Other Work No Long-Running Branches No All-At-Once Switchover

Slide 36

Slide 36 text

Constraints No Blocking Other Work No Long-Running Branches No All-At-Once Switchover No Side Quests

Slide 37

Slide 37 text

Constraints No Side Quests

Slide 38

Slide 38 text

Discovery

Slide 39

Slide 39 text

Discovery * Which default components are being used? * What Bootstrap Javascript is being used? * Is the provided icon set being used, or is it another one? * Are there any Bootstrap extensions being used? * How has the framework been extended or modified? * How much custom CSS depends on the existence of Bootstrap classes? * What Bootstrap classes are being relied on for custom Javascript or tests?

Slide 40

Slide 40 text

Destination

Slide 41

Slide 41 text

Inventory

Slide 42

Slide 42 text

Inventory # Views # Partials # Helpers # Libraries app/views/layouts/application.html.erb app/views/dashboard/index.html.erb app/views/accounts/index.html.erb app/views/admin/accounts/index.html.erb app/views/admin/accounts/show.html.erb app/views/shared/_alert.html.erb app/views/shared/_footer.html.erb app/views/shared/_navbar.html.erb ActivityHelper#activity_model_classes ActivityHelper#activity_event_names simple_form-bootstrap bootstrap-toggle

Slide 43

Slide 43 text

Preliminary Tasks

Slide 44

Slide 44 text

Preliminary Tasks <% admin_context? %> < >< = >Back to Dashboard <% %> < >< = >Admin <% %> if else end li a a li li a a li href href "<%= dashboard_path %>" "<%= admin_root_path %>"

Slide 45

Slide 45 text

Preliminary Tasks module def ** ** end def & = do end = & end end (name, url, , options) tag.li(link_to(name, url, options), [ ]) (name, ) toggle link_to , , { } safe_join [name, tag.span( )] menu tag.ul( , ) tag.li(safe_join([toggle, menu]), ) NavigationHelper nav_link nav_dropdown active: false class: active: class: data: toggle: class: class: class: "#" "dropdown-toggle" "dropdown" "caret" "dropdown-menu" "dropdown"

Slide 46

Slide 46 text

Preliminary Tasks // Form input event listener // … $ ready $ on (document). ( () { ( ). ( , () { }); }); function function ".form-control" "input" # Feature spec using Capybara RSpec with: text: .feature scenario visit new_notes_path fill_in , click_button expect(page).to have_selector( , ) "Note management" "User creates a note" "body" "This is my new note." "Save" "div.alert-success" "Your note was saved." do do end end

Slide 47

Slide 47 text

Setup

Slide 48

Slide 48 text

SEtup class < private def % || end end helper_method params[ ].presence_in( w[bootstrap tailwind]) ApplicationController ActionController::Base css_framework :css_framework :css_framework "bootstrap"

Slide 49

Slide 49 text

SEtup < > < ><%= page_title %> < = > <%= javascript_include_tag %> <% css_framework %> <%= stylesheet_pack_tag %> <%= javascript_pack_tag , %> <% %> <% css_framework %> <%= stylesheet_link_tag %> <%= javascript_pack_tag %> <% %> head title title meta head charset "utf-8" "application" "tailwind" "tailwind" "application" "tailwind" "bootstrap" "application" "application" if == end if == end

Slide 50

Slide 50 text

SEtup # Partials # Helpers app/views/shared/_alert.html.erb app/views/shared/_footer.html.erb app/views/shared/_navbar.html.erb app/views/shared/bootstrap/_alert.html.erb app/views/shared/bootstrap/_footer.html.erb app/views/shared/bootstrap/_navbar.html.erb ActivityHelper#activity_model_classes ActivityHelper#activity_event_names ActivityHelper#bootstrap_activity_model_classes ActivityHelper#bootstrap_activity_event_names

Slide 51

Slide 51 text

SEtup class < private def % || end end class < private def end end helper_method params[ ].presence_in( w[bootstrap tailwind]) ApplicationController ActionController::Base css_framework DashboardController ApplicationController css_framework :css_framework :css_framework "tailwind" "bootstrap" # ...

Slide 52

Slide 52 text

SEtup class < def = super end def = super end private def end end (method, options {}) (method, options.merge( )) (method, options {}) (method, options.merge( )) TailwindFormBuilder ActionView::Helpers::FormBuilder text_field text_area default_classes class: :class class: :class "#{options[ ]} #{default_classes}" "#{options[ ]} #{default_classes}" "w-full border-gray-300 shadow-inner disabled:bg-gray-100 focus:ring-2" # ...

Slide 53

Slide 53 text

SEtup class < end class < end default_form_builder default_form_builder ApplicationController ActionController::Base DashboardController ApplicationController TailwindFormBuilder BootstrapFormBuilder

Slide 54

Slide 54 text

Incremental Steps

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

Take Notes, Not Side Quests

Slide 57

Slide 57 text

Teardown

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

Summary 3 months 30 Pull Requests

Slide 60

Slide 60 text

Billing from User Decoupling

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

Discovery

Slide 63

Slide 63 text

Discovery ### ChangeUserPlan Description: * Updates user plan, and category and name if necessary * Sets user expired_at to nil Called From: * SubscriptionsController#upgrade * StripeEvent#customer_subscription_created * ConvertAccountTypeWorker#process #### EndFreeTrialWorker Description: * Set user trial_ends_at to current time Called From: * Admin::UsersController#end_free_trial

Slide 64

Slide 64 text

Discovery ### UpdateStripeEmailWorker If the user changes their email address, should this still update the billing email in Stripe? ### CreateStripeSubscriptionWorker This worker isn't called from anywhere in the system. Can we safely remove it?

Slide 65

Slide 65 text

Destination

Slide 66

Slide 66 text

Inventory

Slide 67

Slide 67 text

Inventory ### Data * users.stripe_customer_id * users.email * users.coupon * users.trial_ends_at * users.expired_at * users.billing_address ... ### Methods * StripeEventHandlers#charge_succeeded * StripeEventHandlers#customer_subscription_created * StripeEvent#update_trial * CreateStripeCustomer.call * CreateStripeSubscription.call ...

Slide 68

Slide 68 text

Preliminary Tasks

Slide 69

Slide 69 text

Preliminary Tasks class def return if = return if = return if new end end (stripe_charge_id) stripe_charge_id.blank? stripe_customer_id :: .retrieve(stripe_charge_id).customer stripe_customer_id.blank? user .find_by( stripe_customer_id) user.locked_out? . .perform(user.id) .disputed_payment_email(user.id).deliver_now DisputedPaymentWorker perform Stripe Charge User stripe_customer_id: LockOutUserWorker SupportMailer

Slide 70

Slide 70 text

Setup

Slide 71

Slide 71 text

SEtup class < def do end end end [ ] create_table |t| t.references , , , { } t.references , , t.references , , t.string , t.string t.datetime t.datetime t.string ... t.timestamps add_index , , CreateBillingAccounts ActiveRecord::Migration change 7.1 :billing_accounts :account null: false foreign_key: true index: unique: true :plan null: false foreign_key: true :address null: true foreign_key: true :coupon limit: 30 :stripe_customer_id :trial_ends_at :expired_at :email :billing_accounts :stripe_customer_id unique: true

Slide 72

Slide 72 text

SEtup class < def return if end end belongs_to belongs_to has_one , , accepts_nested_attributes_for , account.admin_user.blank? update!( account.admin_user.coupon, account.admin_user.stripe_customer_id, account.admin_user.trial_ends_at, account.admin_user.expired_at, ) Billing::Account ApplicationRecord sync! :account :plan :address as: :addressable dependent: :destroy :address update_only: true false coupon: stripe_customer_id: trial_ends_at: expired_at: ...

Slide 73

Slide 73 text

SEtup namespace desc task :: .in_batches.each_with_index |relation, batch_index| billing_account_ids relation.ids .perform_bulk(billing_account_ids.map { |id| [id] }) (billing_account_id) billing_account :: .find(billing_account_id) billing_account.sync! :billing_accounts sync_all: :environment Billing Account puts 1 SyncBillingAccountWorker Billing Account do do do = + end end end class def = end end "Sync all billing accounts" "Scheduling batch ##{batch_index }, size: #{billing_account_ids.size}" SyncBillingAccountWorker perform

Slide 74

Slide 74 text

Incremental Steps

Slide 75

Slide 75 text

Incremental steps Dual write Change source of truth Remove original

Slide 76

Slide 76 text

Dual write # Before # After # Remove on cleanup class def = = end end class def = = do end end end (user_id) user .find(user_id) customer .call( user) user.update!( customer.id) (account_id) account .find(account_id) customer .call( account.admin_user) :: .transaction account.billing_account.update!( customer.id) account.admin_user.update!( customer.id) CreateStripeCustomerWorker perform CreateStripeCustomerWorker perform User CreateStripeCustomer user: stripe_customer_id: Account CreateStripeCustomer user: ActiveRecord Base stripe_customer_id: stripe_customer_id:

Slide 77

Slide 77 text

Change source of truth # Before # Remove on cleanup # After def return unless && = do end end def return unless && = end user user.expired_at.blank? expiration .current :: .transaction user.update!( expiration) billing_account.update!( expiration) billing_account billing_account.expired_at.blank? expiration .current ... customer_subscription_deleted customer_subscription_deleted Time ActiveRecord Base expired_at: expired_at: Time

Slide 78

Slide 78 text

Remove original # Before # TODO: Remove on cleanup # After class def = do = end end end class def = end end (account_id) account .find(account_id) :: .transaction trial_end .current account.billing_account.update!( trial_end) account.admin_user.update!( trial_end) (account_id) account .find(account_id) account.billing_account.update!( .current) EndFreeTrialWorker perform EndFreeTrialWorker perform Account ActiveRecord Base Time trial_ends_at: trial_ends_at: Account trial_ends_at: Time

Slide 79

Slide 79 text

Discrepancies class < end belongs_to belongs_to has_one , , normalizes , (email) { email.to_s.strip.downcase } ... Billing::Account ApplicationRecord :account :plan :address as: :addressable dependent: :destroy :email with: ->

Slide 80

Slide 80 text

Teardown

Slide 81

Slide 81 text

Summary 6 months 42 Pull Requests

Slide 82

Slide 82 text

a Pattern Perhaps there is

Slide 83

Slide 83 text

Discovery Before During After Destination Inventory Setup Prelim Incremental Steps Teardown

Slide 84

Slide 84 text

Discovery Before During After

Slide 85

Slide 85 text

Before During After Destination

Slide 86

Slide 86 text

Before During After Inventory

Slide 87

Slide 87 text

Before During After Prelim

Slide 88

Slide 88 text

Before During After Setup

Slide 89

Slide 89 text

Before During After Incremental Steps

Slide 90

Slide 90 text

No content

Slide 91

Slide 91 text

Before During After Incremental Steps

Slide 92

Slide 92 text

No content

Slide 93

Slide 93 text

Before During After Teardown

Slide 94

Slide 94 text

Why

Slide 95

Slide 95 text

Why Unrecognized Work

Slide 96

Slide 96 text

Until you make the unconscious conscious, it will direct your life and you will call it fate. Carl Jung

Slide 97

Slide 97 text

Why Unrecognized Work Unfinished Transitions

Slide 98

Slide 98 text

Why Unrecognized Work Unfinished Transitions Left in Limbo

Slide 99

Slide 99 text

No content

Slide 100

Slide 100 text

Lundy, Fastnet, Irish Sea Ive got a message I cant read Im lost at sea Dont bother me Ive lost my way

Slide 101

Slide 101 text

Thank you