Slide 1

Slide 1 text

One engineer company with Ruby on Rails Radoslav Stankov

Slide 2

Slide 2 text

!

Slide 3

Slide 3 text

Radoslav Stankov @rstankov rstankov.com

Slide 4

Slide 4 text

https://tips.rstankov.com

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

" Side project

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

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

What is the most scary thing for a developer? #

Slide 14

Slide 14 text

☕ coffee machine not working What is the most scary thing for a developer? #

Slide 15

Slide 15 text

☕ coffee machine not working % unreproducible bug What is the most scary thing for a developer? #

Slide 16

Slide 16 text

☕ coffee machine not working % unreproducible bug & PM saying change should be easy! What is the most scary thing for a developer? #

Slide 17

Slide 17 text

☕ coffee machine not working % unreproducible bug & PM saying change should be easy! ☎ someone calling on the phone What is the most scary thing for a developer? #

Slide 18

Slide 18 text

☕ coffee machine not working % unreproducible bug & PM saying change should be easy! ☎ someone calling on the phone What is the most scary thing for a developer? #

Slide 19

Slide 19 text

What is the most scary thing for a developer? # ☎ (

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

Business

Slide 25

Slide 25 text

) Default: ALIVE ✅ 0 churn + 500% growth YoY , Self-sustaining business

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

Product-market fit is the point at which you have identified the best target industries, buyers and use cases for your product. Sales become repeatable and scalable. - Product-Market Fit

Slide 29

Slide 29 text

Product-market fit is the point at which you have identified the best target industries, buyers and use cases for your product. Sales become repeatable and scalable. - Product-Market Fit

Slide 30

Slide 30 text

You will likely have to split your main market into many segments and focus on them individually.
 So, the product-market fit will be a continuum of discovering a repeatable sales process for each segment. - Product-Market Fit

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

. Competitors Pen & Paper Excel ProPMS PMPro

Slide 37

Slide 37 text

/ Product journey

Slide 38

Slide 38 text

❌ Started with individual facility managers (B2C) / Product journey

Slide 39

Slide 39 text

❌ Started with individual facility managers (B2C) 1 Pivoted to facility management companies (B2B) / Product journey

Slide 40

Slide 40 text

❌ Started with individual facility managers (B2C) 1 Pivoted to facility management companies (B2B) 2 Non consumption companies / Product journey

Slide 41

Slide 41 text

Pen & Paper Excel ProPMS PMPro Consumption Non consumption

Slide 42

Slide 42 text

❌ Started with individual facility managers (B2C) 1 Pivoted to facility management companies (B2B) 2 Non consumption companies 3 Construction companies / Product journey

Slide 43

Slide 43 text

❌ Started with individual facility managers (B2C) 1 Pivoted to facility management companies (B2B) 2 Non consumption companies 3 Construction companies 4 Complex buildings / Product journey

Slide 44

Slide 44 text

❌ Started with individual facility managers (B2C) 1 Pivoted to facility management companies (B2B) 2 Non consumption companies 3 Construction companies 4 Complex buildings 5 New facility management companies / Product journey

Slide 45

Slide 45 text

❌ Started with individual facility managers (B2C) 1 Pivoted to facility management companies (B2B) 2 Non consumption companies 3 Construction companies 4 Complex buildings 5 New facility management companies ⏫ Target competitors / Product journey

Slide 46

Slide 46 text

❌ Started with individual facility managers (B2C) 1 Pivoted to facility management companies (B2B) 2 Non consumption companies 3 Construction companies 4 Complex buildings 5 New facility management companies ⏫ Target competitors 7 Growing companies / Product journey

Slide 47

Slide 47 text

❌ Started with individual facility managers (B2C) 1 Pivoted to facility management companies (B2B) 2 Non consumption companies 3 Construction companies 4 Complex buildings 5 New facility management companies ⏫ Target competitors 7 Growing companies 8 Stable companies 9 / Product journey

Slide 48

Slide 48 text

❌ Wrong names in database tables ❌ Too traditional User model ❌ Started with individual facility managers (B2C) ❌ Kicking off a service marketplace on top of mobile app ❌ Trying to expand to Spain ❌ Mistakes

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

: Rado's tips

Slide 51

Slide 51 text

: Rado's tips "Find one customer and build around them. Target non-consumption first. Then, find more customers like them."

Slide 52

Slide 52 text

"As a developer, be involved in sales, support, and onboarding of each new category of customers." : Rado's tips

Slide 53

Slide 53 text

"Know the reasons why someone will not use your product. Eliminate those reasons -" : Rado's tips

Slide 54

Slide 54 text

"Get problems from customers and sales. Don't get the solutions." : Rado's tips

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

"Build an intuition about what generic and special cases are in your product." : Rado's tips

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

module SpecialCases extend self ACCOUNT_CUSTOMER1 = 1 ACCOUNT_CUSTOMER2 = 2 ACCOUNT_CUSTOMER3 = 3 def disable_payment_document?(payment) # ... end def receipt_for_deposit_payments?(payment) # ... end def invoice_prefix(account) # ... end def receipt_prefix(account) # ... end def invoice_numbering_changed?(account) # ... end def proforma_title(account)

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

; What to work on next?

Slide 63

Slide 63 text

; What to work on next? < Reduce churn = Bring more customers > Reduce support load ? Quality of life - for customers, for me (DX)

Slide 64

Slide 64 text

; What to work on next? @ Fast track % Critical bugs ☀ 10 minute features or fixes

Slide 65

Slide 65 text

; What to work on next? B Friday (dedicated 2~3h) C Fix exceptions / bugs D Bump dependancies E Pay tech depth

Slide 66

Slide 66 text

; What to work on next? < Reduce churn = Bring more customers > Reduce support load ? Quality of life - for customers, for me (DX) @ Fast track % Critical bugs ☀ 10 minute features or fixes B Friday (dedicated 2~3h) C Fix exceptions / bugs D Bump dependancies E Pay tech depth

Slide 67

Slide 67 text

F Friday all-hands (петъчната оперативка G) • Every Friday afternoon (~2-4h) • Keep company in sync and make decisions • Set goal for next week • Required for everybody • Share company CORE and HEALTH metrics • Share progress of product and support • Support shares common issues • Discussion on major issues • What surprises we encountered • What are the biggest blockers we are facing • sometime brainstorm

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

Technology

Slide 70

Slide 70 text

- one developer (me) H - management portal I - implement a mobile app J - for iOS K / Android L

Slide 71

Slide 71 text

Building mapping M Mobile app J Issue tracker C Employees N Bulletin board ; Notifications O Taxation E Contacts P Omni search Q Reporting R Printing S Bookkeeping T Voting U Financials V Homebook W Debtors X Bulk operations ⚙ Messaging Z Calendar [ Funds \ ePay / EasyPay ] Bank imports ^ Invoicing _ Warranty Issues `
 Technicians D
 Individual accounts a Business accounts M Audit logs /
 Archiving b Trials c Demo d
 I18n G e f
 Marketing site g

Slide 72

Slide 72 text

I Web stack h

Slide 73

Slide 73 text

J Mobile stack h

Slide 74

Slide 74 text

No content

Slide 75

Slide 75 text

https://speakerdeck.com/rstankov/react-mid-game

Slide 76

Slide 76 text

No content

Slide 77

Slide 77 text

No content

Slide 78

Slide 78 text

i Domain

Slide 79

Slide 79 text

563 routes on web 26 screens on mobile app 105 active record models 2176 tests, running for ~3 minutes 3 languages - bg G en f es e I Code metrics

Slide 80

Slide 80 text

No content

Slide 81

Slide 81 text

source: https://refactoring.fm/p/how-to-choose-technology

Slide 82

Slide 82 text

j Make common operations easy k Reduce indirection l Use as much vanilla Rails as possible m Have good e2e test coverage n Avoid writing JavaScript and CSS o Internationalize from day 0

Slide 83

Slide 83 text

No content

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

gem 'annotate' # == Schema Information # # Table name: buildings_print_templates # # id :bigint(8) not null, primary key # format :string not null # name :string not null # options :jsonb not null # created_at :datetime not null # updated_at :datetime not null # account_id :bigint(8) not null # user_id :bigint(8) # # Indexes # # index_buildings_print_templates_on_account_id_and_name (account_id,name) UNIQUE # index_buildings_print_templates_on_user_id (user_id) # # Foreign Keys # # fk_rails_... (account_id => accounts.id) # fk_rails_... (user_id => users.id) # class Buildings::PrintTemplate < ApplicationRecord

Slide 86

Slide 86 text

gem 'counter_culture' class Apartment < ApplicationRecord belongs :building # will track active and archived apartments separately counter_culture :building, column_name: -> { _1.active? ? :apartments_count : :archived_apartments_count } # can do this for the whole account counter_culture %i(building account), column_name: -> { _1.building.active? && _1.active? ? :apartments_count : :arch # can track a sum of a columns counter_culture :building, column_name: :unpaid_taxes_amount, delta_magnitude: -> { _1.unpaid_taxes_amount }
 counter_culture :building, column_name: :deposit_amount, delta_magnitude: -> { _1.deposit_amount } end

Slide 87

Slide 87 text

3 Objects View Components Policy objects Service objects Form objects Query objects

Slide 88

Slide 88 text

3 Objects View Components Policy objects Service objects Form objects Query objects

Slide 89

Slide 89 text

<%= render FieldsetComponent.new(fieldset, title: :bank_account) do %> <%= form.input :bank_account_bank %> <%= form.input :bank_account_iban %> <%= form.input :bank_account_bic %> <% end %>

Slide 90

Slide 90 text

No content

Slide 91

Slide 91 text

https://speakerdeck.com/rstankov/component-driven-ui-with-viewcomponent-gem

Slide 92

Slide 92 text

3 Objects View Components Policy objects Service objects Form objects Query objects

Slide 93

Slide 93 text

KittyPolicy https://github.com/producthunt/kitty-policy

Slide 94

Slide 94 text

module ApplicationPolicy extend self def can_access_account?(user, account) return false if user.nil? user.admin? || account.member?(user) end end

Slide 95

Slide 95 text

module ApplicationPolicy extend KittyPolicy::DSL can :access, Account do |user, account| user.admin? || account.member?(user) end end # generates ApplicationPolicy.can_access_account?(user, account) # can be accessed as ApplicationPolicy.can?(user, :access, account) ApplicationPolicy.authorize!(user, :access, account) # raise KittyPolicy::AccessDenied

Slide 96

Slide 96 text

module ApplicationPolicy extend KittyPolicy::DSL extend ApplicationPolicy::RolesDSL # Role stacking. # Upper role can do everything lower role can do
 # # - director > supervisor > operator > cashier # - chief technician > technician # - chief accountant > accountant building_cashier_can :view, Withdraw building_operator_can :create, Withdraw building_supervisor_can :update, Withdraw building_director_can :destroy, Withdraw # ... end

Slide 97

Slide 97 text

wc -l < app/lib/application_policy.rb 223

Slide 98

Slide 98 text

class CalendarEventsController < ApplicationController require_account_feature :calendar def show @event = find_record Calendar::Event end def edit @event = find_record Calendar::Event, params[:id], authorize: :manage end
 # ... end

Slide 99

Slide 99 text

class CalendarEventsController < ApplicationController require_account_feature :calendar def show @event = find_record Calendar::Event end def edit @event = find_record Calendar::Event, params[:id], authorize: :manage end
 # ... end

Slide 100

Slide 100 text

class CalendarEventsController < ApplicationController require_account_feature :calendar def show @event = find_record Calendar::Event end def edit @event = find_record Calendar::Event, params[:id], authorize: :manage end
 # ... end

Slide 101

Slide 101 text

render template: 'system/feature_unavailable' end rescue_from KittyPolicy::AccessDenied do |error| ErrorReporting.capture_exception(error) render template: 'system/unauthorized' end end class_methods do def require_account_feature(feature_name) self.required_account_feature = feature_name end end private def find_record(scope, id = :none, authorize: :view) record = scope.find(id == :none ? params[:id] : id) ApplicationPolicy.authorize!(current_user, authorize, record) if self.class.required_account_feature.present? Accounts.feature_available!(record, self.class.required_account_feature) end

Slide 102

Slide 102 text

private def find_record(scope, id = :none, authorize: :view) record = scope.find(id == :none ? params[:id] : id) ApplicationPolicy.authorize!(current_user, authorize, record) if self.class.required_account_feature.present? Accounts.feature_available!(record, self.class.required_account_feature) end record end end

Slide 103

Slide 103 text

module Accounts extend self def account_of(record) if record.is_a?(Account) record elsif record.respond_to?(:account) record.account elsif record.respond_to?(:building) record.building.account else raise "Can't find account for #{record.class}##{record.id}" end end def feature_available?(record, feature_name) account_of(record).features.available?(feature_name) end def feature_available!(record, feature_name) return if feature_available?(record, feature_name) raise FeatureUnavailable.new(record, feature_name) end end

Slide 104

Slide 104 text

3 Objects View Components Policy objects Service objects Form objects Query objects

Slide 105

Slide 105 text

module Taxation::PaymentCreate extend self def call(arguments) # ... end private # ... end

Slide 106

Slide 106 text

I don't have many "service objects". In most cases, the "service object", I inline it in a "form object".

Slide 107

Slide 107 text

3 Objects View Components Policy objects Service objects Form objects Query objects

Slide 108

Slide 108 text

MiniForm (Syntax sugar around ActiveModel::Model) https://github.com/RStankov/MiniForm

Slide 109

Slide 109 text

class Calendar::AttendeeForm < ApplicationForm main_model( :attendee, attributes: %i(user_id), read: %i(event building), save: true ) validate :ensure_user_is_allowed def initialize(event) @attendee = event.attendees.new end def user_options @user_options ||= building.account.member_users end def perform Notifications.notify_about(@attendee) end private def ensure_user_is_allowed

Slide 110

Slide 110 text

class ApplicationForm include MiniForm::Model def submit(params) update(params.require(:form).permit(self.class.attribute_names)) end def submit!(params) update!(params.require(:form).permit(self.class.attribute_names)) end end

Slide 111

Slide 111 text

class ApplicationForm include MiniForm::Model def submit(params) update(params.require(:form).permit(self.class.attribute_names)) end def submit!(params) update!(params.require(:form).permit(self.class.attribute_names)) end end module ApplicationHelper def app_form(record, options = {}) options[:as] ||= :form 
 # ... end # ...

Slide 112

Slide 112 text

class CalendarEventAttendeesController < ApplicationController require_account_feature :calendar def new @attendee = Calendar::AttendeeForm.new(find_event) end def create @attendee = Calendar::AttendeeForm.new(find_event) @attendee.submit(params) respond_with @attendee, location: event_path(@attendee.event) end private def find_event find_record Calendar::Event, params[:event_id] end end

Slide 113

Slide 113 text

class CalendarEventAttendeesController < ApplicationController require_account_feature :calendar def new @attendee = Calendar::AttendeeForm.new(find_event) end def create @attendee = Calendar::AttendeeForm.new(find_event) @attendee.submit(params) respond_with @attendee, location: event_path(@attendee.event) end private def find_event find_record Calendar::Event, params[:event_id] end end gem 'responders'

Slide 114

Slide 114 text

I keep business logic in the form object, except if it is going to be used somewhere else. I follow the same logic for ActiveJob objects.

Slide 115

Slide 115 text

3 Objects View Components Policy objects Service objects Form objects Query objects

Slide 116

Slide 116 text

No content

Slide 117

Slide 117 text

Search form

Slide 118

Slide 118 text

Search form Statistics based on search

Slide 119

Slide 119 text

Search form Statistics based on search Search results

Slide 120

Slide 120 text

SearchObject https://github.com/RStankov/SearchObject

Slide 121

Slide 121 text

search = Finance::TransactionsSearch.new(filters: params[:filters]) search.results

Slide 122

Slide 122 text

class Finance::TransactionsSearch def initialize(filters: {}) @filters = filters end def results scope = Transaction.all scope = scope.where(user_id: @filters[:user_id]) if @filters[:user_id] scope = scope.where("amount > ?", @filters[:min_amount]) if @filters[:min_amount] scope = scope.where("amount < ?", @filters[:max_amount]) if @filters[:max_amount] scope = scope.where("created_at > ?", @filters[:start_date]) if @filters[:start_d scope = scope.where("created_at < ?", @filters[:end_date]) if @filters[:end_date] scope end end

Slide 123

Slide 123 text

class Finance::TransactionsSearch def initialize(filters: {}, page: 0) @filters = filters @page = page end def results @results ||= fetch_results end def fetch_results scope = Transaction.all scope = scope.where(user_id: @filters[:user_id]) if @filters[:user_id] scope = scope.where("amount > ?", @filters[:min_amount]) if @filters[:min_amount] scope = scope.where("amount < ?", @filters[:max_amount]) if @filters[:max_amount] scope = scope.where("created_at > ?", @filters[:start_date]) if @filters[:start_d scope = scope.where("created_at < ?", @filters[:end_date]) if @filters[:end_date] scope.page(@page) end end

Slide 124

Slide 124 text

class Finance::TransactionsSearch include SearchObject.module(:kaminary) scope { Transaction.all } option(:user_id) option(:min_amount) { |scope, v| scope.where("amount > ?", v) } option(:max_amount) { |scope, v| scope.where("amount < ?", v) } option(:start_date) { |scope, v| scope.where("created_at > ?", v) } option(:end_date) { |scope, v| scope.where("created_at < ?", v) } end

Slide 125

Slide 125 text

class Finance::TransactionsSearch include SearchObject.module(:kaminary) scope { Transaction.all } option :user_id option :min_amount, with: :filter_by_min_amount option :max_amount, with: :filter_by_max_amount option :start_date, with: :filter_by_start_date option :end_date, with: :filter_by_end_date private def filter_by_min_amount(scope, value) scope.where("amount > ?", value) end def filter_by_max_amount(scope, value) scope.where("amount < ?", value) end def filter_by_start_date(scope, value) scope.where("created_at > ?", value) end

Slide 126

Slide 126 text

search = Finance::TransactionsSearch.new(filters: params[:filters]) search.results 
 search.balance
 search.income
 search.expense
 search.count
 
 search.cashier_options search.source_options search.type_options

Slide 127

Slide 127 text

# Where do all those objects live?

Slide 128

Slide 128 text

app/policies app/services app/forms app/queries

Slide 129

Slide 129 text

No content

Slide 130

Slide 130 text

app/policies app/services app/forms app/queries

Slide 131

Slide 131 text

app/policies app/services app/forms app/queries app/network

Slide 132

Slide 132 text

app/policies app/services app/forms app/queries app/network app/validators

Slide 133

Slide 133 text

app/policies app/services app/forms app/queries app/network app/validators
 app/decorators

Slide 134

Slide 134 text

app/policies app/services app/forms app/queries app/network app/validators
 app/decorators app/presenters

Slide 135

Slide 135 text

app/services app/forms app/queries app/network app/validators
 app/decorators app/presenters app/notifiers

Slide 136

Slide 136 text

app/forms app/queries app/network app/validators
 app/decorators app/presenters app/notifiers app/values

Slide 137

Slide 137 text

app/queries app/network app/validators
 app/decorators app/presenters app/notifiers app/values app/support

Slide 138

Slide 138 text

app/network app/validators
 app/decorators app/presenters app/notifiers app/values app/support app/others

Slide 139

Slide 139 text

No content

Slide 140

Slide 140 text

put everything in one folder put everything in one folder put every object type in its own folder

Slide 141

Slide 141 text

app/lib
 app/lib/[domain]/*.rb
 app/lib/[domain].rb

Slide 142

Slide 142 text

You only get special folder if you called in special way. Those are the special folders app/components app/controllers app/jobs app/models app/graph app/mailers

Slide 143

Slide 143 text

No content

Slide 144

Slide 144 text

No content

Slide 145

Slide 145 text

No JavaScript (1030 lines without any libraries)

Slide 146

Slide 146 text

require 'features_helper' feature 'Feature: Calendar' do scenario 'attendee - create > list > delete' do building = create :building sign_in_operator building account_member = create :account_member, account: building.account event = create :calendar_event, building: building visit event_path(event) click_on I18n.t(:action_new_attendee) # test: validation submit_form expect_form_errors :blank # test: create submit_form!( user_id: account_member.user_id, )

Slide 147

Slide 147 text

submit_form expect_form_errors :blank # test: create submit_form!( user_id: account_member.user_id, ) expect(page).to have_content account_member.name attendee = event.attendees.last! expect(attendee.user).to eq account_member.user # test: destroy click_on_destroy expect_flash_message :destroy expect_to_be_destroyed attendee end end

Slide 148

Slide 148 text

sign_in_operator submit_form submit_form! click_on_destroy expect_form_errors expect_flash_message expect_to_be_destroyed p Custom test helpers

Slide 149

Slide 149 text

https://tips.rstankov.com/p/my-testing-tips

Slide 150

Slide 150 text

Recap

Slide 151

Slide 151 text

No content

Slide 152

Slide 152 text

No content