Dat ene kleine extraatje

Een klant met een draaiend systeem vraagt iets kleins. Ze willen het onderhoud van hun machines bijhouden naast hun orders. Welke machine, datum laatste onderhoud, datum volgende onderhoud, wie het deed, een notitie, en een overzicht van wat deze maand vervalt.

Je maakt de eerlijke raming en er komt drie weken uit, en het gezicht van de klant doet wat gezichten dan doen.

Ze hebben gelijk dat ze verrast zijn. In woorden is het één tabel met zes kolommen. Die drie weken zijn niet de zes kolommen. Het zijn de migratie, het model, de endpoints voor aanmaken, wijzigen en verwijderen, validatie met foutmeldingen die op het juiste formulierveld landen, de lijstpagina met sorteren en pagineren en een zoekvak, de detailpagina, rechten per rol, de filters, de export, het audittraject, de tests, en de plek in de navigatie waar het moet verschijnen.

Een ontwikkelaar en een klant gebogen over een laptop en een geprint blad aan een bureau in een fabriekskantoor, met zicht op de werkvloer door het raam Wat iemand gevraagd heeft, is klein. De stapel erachter is waar de raming uit bestaat.

Tel nu eens hoeveel daarvan specifiek is voor machineonderhoud.

Het antwoord zijn de zes kolommen. Al de rest is hetzelfde werk dat je deed voor orders, en voor klanten, en voor offertes, en voor de ticketmodule van afgelopen lente. Je hebt dezelfde lijstpagina elf keer geschreven met andere woorden erin.

De categorie die dit decennia geleden al oploste

Er bestaat een familie bedrijfssystemen die de andere weg koos, en ze hadden gelijk over de structuur, ook waar ze over andere dingen ongelijk hebben.

Hun eigenschappen zijn het waard om plat te benoemen, want ze verklaren waarom bedrijven er vijftien jaar op blijven. Alles zit in één samenhangend systeem in plaats van in negen losse abonnementen die elk een stuk van dezelfde klant bezitten. Elk object erin is op dezelfde manier gemodelleerd, dus wie één scherm leert kennen, kent ze allemaal. Het is per bedrijf aanpasbaar zonder het product te forken, dus jouw specifieke zaken worden geen onderhoudslast die niemand wil dragen. En het blijft van jou: het model, de data en de uitbreidingen zijn dingen die je bezit in plaats van huurt.

Wat we niet wilden, was het gewicht, het jargon, en een stack waarin we gast zouden zijn. Dus bouwden we dezelfde vorm op onze eigen fundamenten, en noemden het It’s About Business.

Het domein is data, geen code

Er is geen ordertabel. Er is geen klantentabel. Er is geen onderhoudstabel.

Er zijn entiteitsdefinities, velddefinities, weergavedefinities, en records waarvan de inhoud JSON is, gesleuteld op veld. Eén generieke API bedient elke entiteit. Eén generieke renderer tekent elk scherm. Het type van een veld - tekst, getal, keuzelijst, relatie naar een andere entiteit, berekende formule, optelling over onderliggende records, volgnummer - bepaalt hoe het valideert, hoe het in een formulier verschijnt en hoe het in een lijst getoond wordt, en die beslissing staat op exact één plek in plaats van in elke pagina.

bezitbezitvoldoet aanleest at runtimeleest at runtimeEntiteitsdefinitieVelddefinitieWeergavedefinitieRecord (JSONB)Generieke rendererGenerieke APILijstKanbanDetailformulierZelfde endpoints
Een bedrijfsobject toevoegen voegt rijen toe aan dit plaatje. Geen code.

Het gevolg is de hele pointe. Onderhoud toevoegen voegt geen migratie, geen endpoint, geen pagina en geen test toe. Het voegt definities toe. De lijstweergave, het detailformulier, de validatie, het zoeken, het audittraject en het menu-item komen uit de engine, want die kwamen daar voor al de rest ook vandaan.

Drie weken wordt een namiddag.

Niet omdat iemand sneller werkte, maar omdat het meeste werk opgehouden is te bestaan.

Packs, en waarom ze samenwerken

Definities worden geleverd als packs: gewone TypeScript-data die entiteiten, velden, weergaven, startrecords, automatiseringen en menu-items declareert. Een pack installeren zet dat allemaal weg in volgorde van afhankelijkheid.

Het interessante is dat packs stapelen. Een basispack levert een capaciteit - “er is hier iets dat een klant is” - en andere packs verklaren dat ze die nodig hebben en enten dan hun eigen velden en weergaven op diezelfde gedeelde entiteit. Installeer een supportpack naast een offertepack en beide hangen aan één klant, niet aan twee klanten met een synchronisatieprobleem ertussen.

Elke veld- en weergaverij draagt het pack dat ze bijgedragen heeft. Eén pack opnieuw synchroniseren vervangt enkel zijn eigen bijdrage en laat die van de anderen staan. Een rij zonder pack is een eigen aanpassing van de klant, en de synchronisatie is verplicht die te bewaren.

Die laatste zin is de volledige eigenschap “aanpasbaar zonder forken”, uitgedrukt als een kolom die leeg mag zijn. Een bedrijf voegt drie eigen velden toe aan de factuurentiteit. De leverancier levert een upgrade van het factuurpack. De upgrade vervangt de leveranciersvelden en raakt die drie niet aan. Niemand hoeft iets te mergen.

Twee invarianten die we op de harde manier leerden

Definities synchroniseren moet atomair zijn. De velden van een pack vervangen is verwijderen gevolgd door aanmaken, en een vroege versie daarvan liep buiten een transactie. Een conceptpack faalde halverwege de installatie op validatie, en een live entiteit bleef achter met gestripte velden. De records stonden er allemaal nog. Elke kolom was leeg. Het zag er exact uit als dataverlies, en tien minuten lang geloofde iedereen dat ook.

Wat ons bij de tweede brengt.

Records en definities staan los van elkaar. Velddefinities lezen of schrijven nooit recorddata. Schrap een veld en de records houden de sleutel in hun JSON. Daarom was dat incident te herstellen met één herinstallatie: de definities kwamen terug en de data had er de hele tijd ongemoeid onder gelegen.

Het inzaaien van startdata is om dezelfde reden idempotent. Heeft een entiteit al records, dan doet het niets. Een pack opnieuw installeren is dus de standaard herstelprocedure in plaats van een gevaarlijk laatste redmiddel, wat een prettige eigenschap is voor een herstelprocedure.

Waar het terugvecht

Dit is niet gratis, en de valkuilen zijn specifiek.

Een ateliermachine die identieke onderdelen uitspuwt terwijl een operator één afwijkend stuk tegen een werklamp houdt, krullen en handgereedschap rond het bed Een machine die het gewone geval goedkoop maakt, maakt het ongewone geval lastig.

Een generieke renderer heeft een plafond. Negentig procent van de bedrijfsschermen is een lijst, een formulier en een bord, en de engine doet die beter dan een pagina op maat, omdat verbeteringen overal tegelijk landen. De overige tien procent is echt maatwerk, en elk daarvan is een gevecht. Ofwel breid je de engine uit voor een geval met één gebruiker, ofwel aanvaard je een scherm dat voor 80 procent klopt. Beide antwoorden zijn soms juist en geen van beide voelt goed.

Bevragen over JSON vraagt aandacht. Serverzijdig lijsten, zoeken, sorteren en facetteren over een JSON-kolom werkt goed en is niet gratis. Indexeringsstrategie wordt een echt onderwerp in plaats van iets dat je ORM voor je regelt.

Berekende velden en optellingen worden per pagina resultaten geëvalueerd. Dat begrenst hoe duur ze mogen zijn. Een optelling die per record duizend onderliggende rijen aggregeert, is een ontwerpfout die je ontdekt bij het laden van een pagina.

Debuggen schuift een niveau omhoog. Klopt een scherm niet, dan zit de fout zelden in het scherm. Ze zit in een definitie, of in het pack dat de definitie schreef, of in de volgorde waarin twee packs geïnstalleerd zijn. Daarover redeneren is oprecht moeilijker dan een component met een bug erin, en het is een echte belasting voor iedereen die de engine niet zelf gebouwd heeft.

Een engine bouwen duurt veel langer dan de applicatie één keer bouwen. Hadden we één systeem voor één bedrijf nodig gehad, dan was dit een ruime misrekening geweest. Het betaalt pas terug over veel entiteiten, veel packs en jaren.

Eén bewuste beperking

Schema’s kunnen door een model opgesteld worden. Beschrijf wat je nodig hebt en een agent produceert een pack, dat je daarna verfijnt en installeert.

Wat het product bewust níet heeft, is live bewerken van entiteits-, veld- en weergavedefinities in de interface. Geen beheerscherm waar een gebruiker met een klik een veld toevoegt aan een levende entiteit.

Die beperking is onpopulair in de eerste demo en correct tegen de derde maand. Definities hebben de vorm van code, ook al zitten ze opgeslagen als data: ze hebben review nodig, versiebeheer, een diff, en de mogelijkheid om eerst hetzelfde in een testworkspace te installeren. Systemen waar iedereen op een dinsdagnamiddag een veld kan toevoegen, eindigen met entiteiten van veertien velden waarvan er vier hetzelfde betekenen, en niemand die kan zeggen welke.

Een pack is een reviewbaar artefact. Een klik niet.

De vraag die telt

Niet “bouwen of kopen”. De nuttige vraag is: hoeveel verschillende bedrijfsobjecten moet dit bedrijf de komende vijf jaar gemodelleerd krijgen, en hoe hard wijken die af van wat om het even welk product standaard meelevert?

Eén object met een standaardvorm: kopen. Een handvol met standaardvormen: koop het samenhangende systeem en configureer het.

Een dozijn objecten, waarvan er meerdere specifiek genoeg zijn dat geen enkele standaard past, en een bedrijf dat van plan is ze te blijven veranderen: dan is de prijs die telt niet de eerste module. Het is de elfde. Zorg dat die een namiddag kost en de rekensom voor al de rest verandert mee.

It’s About Business is waar de onze uitkwam.