35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Technische Due Diligence op een Rails Codebase: Wat Ik Echt Controleer

Technische Due Diligence op een Rails Codebase: Wat Ik Echt Controleer

Roger Heykoop
Fractional CTO, Ruby on Rails
Voordat je een bedrijf overneemt of start als fractional CTO — dit is de Rails-codebase audit die ik elke keer uitvoer, en wat de bevindingen doorgaans betekenen.

Zes maanden geleden belde een private equity-firma me over een SaaS-overname. Middelgrote Rails-applicatie, zo’n twaalf jaar oud, 40.000 betalende klanten, vraagprijs in de acht cijfers. Het financiële due diligence was gedaan. Men wilde dat iemand naar de code keek.

Ik had een week. Dit is wat ik bekeek.

Eerst de Gemfile

Voordat ik ook maar één model of controller lees, open ik de Gemfile. Ik zoek niet naar de lijst van gems — maar naar de vorm ervan.

Een gezonde Gemfile heeft een heldere structuur. Production-dependencies, testdependencies, development-tools, netjes gescheiden. De gem-versies zijn vastgepind of hebben verstandige constraints. Er is een recente Gemfile.lock gecommit.

Deze applicatie had 187 gems. Sommige waren uitgecomment maar niet verwijderd. Meerdere dupliceerden functionaliteit — twee gems voor paginering, drie voor authenticatie (geen van alle Devise), één voor “bestandsuploads” die al zes jaar niet meer bijgewerkt was. De Rails-versie was 6.0. Huidig is 8.0.

Een verouderde Gemfile vertelt je iets over de cultuur. Ofwel doet het team geen reguliere onderhoudsslagen, ofwel zijn ze te bang om dingen te breken om dependencies te verwijderen. Beide zijn problemen, maar verschillende. Het eerste is nalatigheid. Het tweede is angst — en angst betekent doorgaans onvoldoende testcoverage.

Ik check ook bundle outdated en let op de staart: gems die al twee jaar geen nieuwe release hebben gehad maar nog steeds actief in gebruik zijn. Dat zijn toekomstige securityrisico’s. Dan kijk ik naar gems met native extensions, omdat die de deployment compliceren en subtiele versieconflicten kunnen veroorzaken die alleen om twee uur ‘s nachts op een dinsdag boven water komen.

Migraties Vertellen de Geschiedenis

ls -la db/migrate/ | wc -l

Deze app had 847 migratiebestanden. Ik steekproefde er elke honderdste. Wat ik zoek: zijn ze omkeerbaar? Gebruiken ze disable_ddl_transaction! waar dat nodig is? Zijn er change_column-aanroepen die een volledige table rewrite op een live database hadden veroorzaakt?

Migratiehygiëne weerspiegelt deployment-discipline. Als ik migraties vind die duidelijk productiedowntime hadden veroorzaakt — het toevoegen van NOT NULL-kolommen zonder default-waarden op grote tabellen, ontbrekende algorithm: :concurrently op indexen — dan weet ik dat het team óf gepland onderhoud had, óf snel schipte en incidenten voor lief nam.

# De verkeerde manier — vergrendelt de volledige orders-tabel bij 50 miljoen rijen
add_index :orders, :customer_id

# De juiste manier
class AddCustomerIdIndexToOrders < ActiveRecord::Migration[7.1]
  disable_ddl_transaction!

  def change
    add_index :orders, :customer_id, algorithm: :concurrently
  end
end

Geen van beide antwoorden is automatisch diskwalificerend. Maar ik moet weten welk patroon het team volgde, omdat dat het risicoprofiel van toekomstig schemawerk bepaalt.

Ik kijk ook naar het schema als geheel. Deze app had 94 tabellen. Dat is niet buitensporig voor een twaalf jaar oud product, maar ik zag 11 tabellen met minder dan 50 rijen in productie die eruitzagen als restanten van features die stil waren verschenen en stil waren gestorven. Dode schema’s zijn technisch gezien onschadelijk, maar het is een signaal: niemand doet aan opruimen.

Testcoverage: Het Getal Liegt

Het team vertelde me dat ze 87% testcoverage hadden. Dat klinkt goed. Dat is het niet per se.

Ik draaide de suite:

time bundle exec rails test

Vier minuten drieëntwintig seconden. Voor een twaalf jaar oude app met 87% coverage is dat óf een heel slanke testsuite, óf een heel snelle machine. Bleek het eerste — 1.100 tests, bijna allemaal unit tests, bijna geen integratietests. Het coveragegetal kwam van het aanroepen van individuele methoden in isolatie.

Wat ontbrak: tests die de volledige stack aanspreken. Controller tests die autorisatie verifiëren, niet alleen responscodes. Integratietests die simuleren wat een klant daadwerkelijk doet over meerdere stappen. Tests voor de facturatiestroom — want dáár zijn bugs het duurst.

Hoge unit coverage met lage integratiecoverage is volgens mij slechter dan het omgekeerde. Unit tests vertellen je dat je afzonderlijke componenten in isolatie werken. Ze vertellen je niet dat je systeem werkt. Het systeem is wat je klanten ervaren.

Ik check ook de CI-configuratie. Als tests alleen op de main-branch draaien, vangt het team geen regressies op vóór ze mergen. Als de suite intermittent falende tests heeft die worden onderdrukt of overgeslagen met skip-comments en een TODO van twee jaar oud — dan is dat technische schuld met tanden.

Databasegezondheid in Vijf Queries

Ik vraag om read-only toegang tot de productiedatabase. Als ze dat weigeren, vraag ik om de output van deze queries:

-- Tabelbloat: heeft autovacuum bijgehouden?
SELECT relname, n_dead_tup, n_live_tup,
       round(n_dead_tup::numeric / nullif(n_live_tup, 0) * 100, 2) as dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;

-- Trage queries, gesorteerd op gemiddelde uitvoeringstijd
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;

-- Ontbrekende indexen: sequential scans op grote tabellen
SELECT schemaname, relname, seq_scan, seq_tup_read,
       idx_scan, seq_tup_read / seq_scan AS avg_tuples_per_scan
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_tup_read DESC
LIMIT 10;

-- Op dit moment langlopende queries
SELECT pid, now() - query_start AS duration, query, state
FROM pg_stat_activity
WHERE (now() - query_start) > interval '30 seconds'
AND state != 'idle';

Deze app had significante tabelbloat op de events-tabel — 40% dead tuples. Autovacuum kon het niet bijhouden, waarschijnlijk omdat de tabel voortdurend schrijfoperaties ontving. Een gebloate tabel verslechtert indexperformance over tijd. Geen noodsituatie, maar ook niet gratis om te verhelpen.

De trage query-log toonde zeventien queries met een gemiddelde van meer dan 200ms. Drie waren klassieke N+1-patronen. Veertien waren legitiem trage queries — aggregaties over grote tijdsbereiken — die index-tuning of query-rewrites nodig hadden. Dat is meerdere weken aan databasewerk dat in geen enkel financieel model opduikt.

Databasegezondheid is het snelste signaal van operationele discipline. Een nette database betekent dat het team geeft om performance. Een gebloate, trage database betekent dat ze features shippen en het motorwaarschuwingslampje negeren.

Het Deploymentverhaal

Hoe komt code van de laptop van een developer naar productie? Ik vraag om het deploymentproces te zien, niet de documentatie — ik kijk toe hoe iemand het daadwerkelijk uitvoert, of ik lees de echte CI/CD-configuratie.

Deze app werd uitgerold via een shellscript dat via SSH op drie servers inlogde en git pull uitvoerde. Geen stagingomgeving. Geen blue-green deployment. Geen health checks voordat verkeer de nieuwe code raakte. Deployments vonden plaats tijdens kantooruren.

Dat is angstaanjagend. Eén slechte commit raakt alle drie de servers tegelijk. Er is geen smoke test voordat klanten de nieuwe code zien. Er is geen rollback-strategie buiten git revert en hopen dat je het sneller kunt dan de supporttickets zich opstapelen.

Wat ik wil zien: geautomatiseerde deployments getriggerd door een geslaagde CI, minimaal een stagingomgeving, een vorm van health check voordat verkeer verschuift, en een gedocumenteerde rollback-procedure. Kamal, Capistrano, Heroku releases, GitHub Actions — het hulpmiddel maakt niet uit. De discipline wel.

Het ontbreken van een stagingomgeving is de bevinding die niet-technische kopers het meest verrast. “Maar de developers testen toch lokaal voor ze pushen,” zeggen ze. Ja. En lokale omgevingen zijn geen productie. Dat zijn ze nooit geweest.

Security: De Niet-Onderhandelbare Zaken

Ik doe geen penetratietest in een week. Maar ik check de voor de hand liggende dingen.

Brakeman. Uitvoeren, elke bevinding lezen:

bundle exec brakeman -q

Deze app had 12 hoog-risicobevindigingen en 31 gemiddeld-risicobevindigingen. De hoge omvatten twee potentiële SQL-injectiekwetsbaarheden via stringinterpolatie in raw queries. Het soort dingen dat vijf minuten review had kunnen ondervangen. Iemand had geschreven:

# Gevaarlijk — gebruikersinvoer direct in SQL-string
User.where("name = '#{params[:name]}'")

# Veilig
User.where(name: params[:name])

Dat is Rails-basiskennis. Het aantreffen ervan in een volwassen codebase vertelt je iets over de reviewcultuur.

Credential-opslag. Staan geheimen in omgevingsvariabelen of in de codebase? Ik heb API-sleutels zien committen in de git-geschiedenis. Zodra ze daar staan, zijn ze gecompromitteerd — ook als je ze later verwijdert, want git-geschiedenis is permanent tenzij je hem herschrijft:

# Controleer git-geschiedenis op gecommitte geheimen
git log --all -S "password" --oneline | head -20
git log --all -S "secret_key" --oneline | head -20

Dependency-kwetsbaarheden:

bundle exec bundler-audit check --update

Deze app had drie gems met bekende CVE’s. Twee middelmatig ernstig, één hoog. Alle drie hadden gepatchte versies beschikbaar. Niemand had het in vier maanden opgemerkt.

Autorisatiegrenzen. Zijn adminroutes beschermd op controller-niveau, niet alleen op view-niveau? Kan ik /admin bereiken door de URL te typen, ook als er geen link naartoe is? In deze app: ja. Twee admin-endpoints waren alleen beveiligd door de navigatielink te verbergen. Security through obscurity is geen security.

De Busfactor

De busfactor is het antwoord op: hoeveel mensen moeten vertrekken voordat deze codebase onbeheersbaar wordt?

Ik kijk naar de git-log:

git shortlog -sn --all | head -20

Deze app had 14 bijdragers in de loop van de tijd. Twee van hen waren verantwoordelijk voor 73% van de commits. Eén was een jaar eerder vertrokken. De ander was de CTO die in het kader van deze overname vervangen werd.

Dan kijk ik welke bestanden slechts één bijdrager hebben:

git log --format="%ae" -- app/models/subscription.rb | sort -u

subscription.rb was aangeraakt door precies één persoon. Die persoon vertrok. Het abonnementsmodel is het belangrijkste bestand in een SaaS-applicatie.

Busfactoranalyse gaat niet over schuld. Het gaat over risico. Geconcentreerde kennis die in iemands hoofd leeft in plaats van in documentatie, tests of leesbare code, is een aansprakelijkheid die niet op de balans verschijnt. Ze duikt drie maanden na de overname op, als de sleutelfiguur weg is en niemand kan uitleggen waarom de prorateringslogica werkt zoals ze werkt.

Wat de Code Je Niet Kan Vertellen

De belangrijkste vraag staat niet in de codebase: wat kost het om een nieuwe developer in te werken?

Vraag om de ontwikkelomgeving from scratch op te zetten, alleen aan de hand van de gedocumenteerde instructies. Als het meer dan twee uur duurt, of als de README stappen heeft die niet meer werken, kijk je naar een team dat al een tijdje niemand nieuw heeft ingewerkt. In een twaalf jaar oude app betekent dat óf het team is bijzonder stabiel (mogelijk een goed teken), óf er heeft niemand nieuw geprobeerd in te stappen (mogelijk een slecht teken).

Praat met de developers die blijven. Niet over de code — over wat hen frustreert. Waar zien ze tegenop bij het shippen? Welke delen van de codebase vermijden ze aan te raken? Die antwoorden lokaliseren je hoogste risicogebieden sneller dan welk geautomatiseerd analysetools dan ook.

Eén vraag die ik altijd individueel stel, nooit in een groep: “Als je één deel van dit systeem mocht herschrijven, wat zou dat zijn en waarom?” De consistentie van de antwoorden binnen een team vertelt je waar de echte probleemgebieden zitten. Als drie verschillende engineers onafhankelijk van elkaar hetzelfde subsysteem noemen, dáár ligt jouw technische risico na de overname.

Wat Dit Onderzoek Concludeerde

De PE-firma ontving mijn rapport twee dagen voor de deadline. Mijn samenvatting: de codebase is overneembaar, maar niet tegen de vraagprijs. De securitybevindigingen vereisten onmiddellijke verhelping. Het testcoveragecijfer was misleidend — de daadwerkelijke coverage van kritieke paden was veel lager. Het deploymentproces had een volledige herziening nodig voordat het team veilig kon opschalen. De busfactor op het abonnementsmodel was een reëel bedrijfsrisico, niet alleen een technisch risico.

Ze gebruikten het rapport om opnieuw te onderhandelen. De deal sloot tegen een lagere prijs. Ik bracht de volgende drie maanden door met het verhelpen van de dingen die ik had geïdentificeerd.

Na negentien jaar Rails heb ik dit soort beoordeling zo’n dertig keer gedaan. De details variëren. De patronen niet. De meeste problemen in een volwassen Rails-codebase zijn niet verrassend — het zijn voorspelbare accumulaties van shortcuts die op het moment zelf logisch leken. Begrijpen welke shortcuts er zijn genomen, en waarom, vertelt je meer over wat de overname werkelijk gaat kosten dan welke post in het financiële model dan ook.


Veelgestelde Vragen

Hoe lang duurt een grondige technische due diligence?

Voor een serieuze pre-overnameanalyse budget ik minimaal vijf tot zeven dagen: twee dagen voor geautomatiseerde analyse en code-review, twee dagen voor database- en infrastructuurbeoordeling, één tot twee dagen voor ontwikkelaarsgesprekken en het schrijven van het rapport. Haast produceren rapporten die zelfverzekerd klinken maar belangrijke zaken missen. Als je binnen 48 uur een eerste indruk nodig hebt, is dat haalbaar — maar wees duidelijk dat het voorlopig is.

Kun je due diligence doen zonder toegang tot de productiedatabase?

Je kunt gedeeltelijk due diligence doen. Code-review, statische analyse, testsuite en CI-configuratie zijn allemaal toegankelijk zonder productietoegang. Wat je mist is echte performancedata — werkelijke tabelgroottes, queryperformance onder belasting, vacuümgezondheid. Als het doelbedrijf geen read-only productietoegang wil geven, vraag dan om een pg_dump --schema-only plus de output van de diagnostische queries hierboven. Hun bereidheid om die output te verstrekken vertelt je ook iets.

Wat is de meest voorkomende bevinding die kopers verrast?

Testcoveragecijfers die er goed uitzien maar de verkeerde dingen dekken. Een koper ziet “87% testcoverage” en gaat ervan uit dat de codebase solide is. Wat dat getal doorgaans betekent: 87% van de methoden werd minstens één keer aangeroepen tijdens de testrun. Het zegt niets over of de tests correct gedrag verifiëren, regressies opvangen, of de kritieke paden testen die er werkelijk toe doen voor het bedrijf. Vraag altijd om een coverage-uitsplitsing per directory, en vraag altijd wat er specifiek niet gedekt is.

Een Rails-overname evalueren, of op het punt staan een bedrijf te vervoegen als technisch leider? TTB Software doet onafhankelijke technische due diligence en fractional CTO-opdrachten. Negentien jaar Rails, zo’n dertig beoordelingen, geen verrassingen die we nog niet gezien hebben.

#rails #due-diligence #architecture #fractional-cto #code-review #technical-leadership
R

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation