I samband med interagerande tjänstemoduler uppstår den oundvikliga frågan: Enligt vilka regler sker kommunikation? I IT-produkter representerar ett "kontrakt" en formell förståelse av vilka data som flyter mellan systemen och hur de överförs. Detta innebär dataformat (JSON, Protobuf, etc.), strukturella element (fält, datatyper), kommunikationsprotokoll (REST, gRPC, meddelandeköer) och andra specifikationer.
Ett kontrakt säkerställer öppenhet (alla vet vad som tas emot och skickas), förutsägbarhet (vi kan uppdatera kontraktet och underhålla versioner) och tillförlitlighet (vårt system kommer inte att misslyckas om vi gör välskötta ändringar).
I praktiken, även om alla pratar om mikrotjänster, "kontrakt" och API:er, ser vi ofta människor anta tillvägagångssättet: "Varför inte skapa en delad tabell i databasen istället för att bygga API:er?"
Därför, även om användningen av en delad tabell för datautbyte kan verka effektiv och optimerad för snabba resultat, genererar det olika tekniska och organisatoriska utmaningar på lång sikt. Men när team väljer delade tabeller för datautbyte kan de möta många problem under implementeringen.
När tjänster kommunicerar via REST/gRPC/GraphQL har de en formell definition: OpenAPI (Swagger), protobuf-scheman eller GraphQL-scheman. Dessa definierar i detalj vilka resurser (slutpunkter) som är tillgängliga, vilka fält som förväntas, deras typer och format för begäran/svar. När 'ett delat bord' fungerar som ett kontrakt finns det ingen formell beskrivning: Det finns ingen formell beskrivning av kontraktet; endast tabellschemat (DDL) är tillgängligt och även det är inte väl dokumenterat. Alla mindre ändringar av tabellstrukturen (t.ex. lägga till eller ta bort en kolumn, ändra datatyper) kan påverka andra team som läser från eller skriver till denna tabell.
API-versionering är en normal praxis: Vi kanske har v1, v2 och så vidare, och vi kan behålla bakåtkompatibiliteten och sedan gradvis flytta klienter till de nyare versionerna. För databastabeller har vi bara DDL-operationer (t.ex. ALTER TABLE
), som är tätt kopplade till en specifik DB-motor och kräver noggrann hantering av migrering.
Det finns inget centraliserat system som kan skicka varningar till konsumenter om schemaändringar som kräver att de uppdaterar sina frågor. Som ett resultat kan "under-the-table"-affärer uppstå: Någon kan posta i en chatt, "I morgon ändrar vi kolumn X till Y", men det finns ingen garanti för att alla kommer att vara redo i tid.
När det finns ett tydligt definierat API är det uppenbart vem som äger det: tjänsten som fungerar som API-utgivare. När flera team använder samma databastabell uppstår förvirring om vem som får bestämma strukturen och vilka fält som ska lagras och hur de ska tolkas. Som ett resultat kan bordet bli "ingens egendom", och varje förändring blir ett uppdrag: "Vi måste kolla med det andra laget ifall de använder den gamla kolumnen!"
Det är svårt att hålla reda på vem som kan läsa och skriva till en tabell om många lag har tillgång till DB. Det finns en chans att obehöriga tjänster kan komma åt uppgifterna trots att det inte var avsett för dem. Det är lättare att hantera sådana problem med ett API: Du kan kontrollera åtkomsträttigheterna (vem kan anropa vilka metoder), använda autentisering och auktorisering och övervaka vem som ringde vad. Med ett bord är det mycket mer komplicerat.
Alla interna ändringar av data (omorganisering av index, partitionering av tabellen, ändring av DB) blir ett globalt problem. Om tabellen fungerar som ett publikt gränssnitt kan ägaren inte göra interna ändringar utan att äventyra alla externa läsare och skribenter.
Detta är den mest smärtsamma aspekten: Hur går man tillväga för att informera ett annat team om att schemat kommer att ändras nästa dag?
När flera lag använder en delad tabell för att välja och uppdatera kritisk data, kan det lätt bli ett "slagfält". Resultatet är att affärslogik hamnar utspridda över olika tjänster och det finns ingen centraliserad kontroll av dataintegriteten. Det blir väldigt svårt att veta varför ett visst fält lagras på ett visst sätt, vem som kan uppdatera det och vad som händer om det lämnas tomt.
Anta till exempel att tabellen går sönder: Låt oss säga att det finns dålig data eller att någon har låst några viktiga rader. Att identifiera källan till problemet kan ofta kräva att man ber alla team med DB-åtkomst att avgöra vilken fråga som orsakade problemet. Det är ofta inte uppenbart: Det betyder att ett teams fråga kan ha låst databasen, medan ett annat teams fråga producerar det observerbara felet.
En delad databas är en enda felpunkt. Om det går ner, kommer många tjänster att gå ner med det. När databasen har problem med prestanda på grund av en tjänsts tunga frågor upplever alla tjänster problem. I en modell med tydliga API:er och dataägande är varje team mästare på sin tjänsts tillgänglighet och prestanda, så ett fel i en komponent sprids inte till andra.
En vanlig kompromiss är: "Vi ger dig en skrivskyddad replika så att du kan fråga utan att påverka vår huvuddatabas." Till en början kan det lösa vissa belastningsproblem, men:
Modern designpraxis (till exempel "API First" eller "Contract First") börjar med en formell gränssnittsdefinition. OpenAPI/Swagger-, protobuf- eller GraphQL-scheman används. På så sätt vet både människor och maskiner vilka slutpunkter som är tillgängliga, vilka fält som krävs och vilka datatyper som används.
I en mikrotjänster (eller till och med modulär) arkitektur, är antagandet att varje tjänst äger sin data helt. Den definierar struktur, lagring och affärslogik och tillhandahåller ett API för all extern åtkomst till det API. Ingen kan röra "någon annans" databas: bara officiella slutpunkter eller händelser. Detta gör livet lättare närhelst förändringar är i fråga och det är alltid tydligt vem som bär skulden.
GET /items
, POST /items
, etc., och klienter gör förfrågningar med ett väldefinierat dataschema (DTO).
Oavsett vilken modell är det både möjligt och väsentligt att implementera versionskontroll på gränssnittet. Till exempel:
En grundläggande princip är att teamet som äger data får bestämma hur de ska lagras och hanteras, men de ska inte ge direkt skrivåtkomst till andra tjänster. Andra måste gå igenom API:t i motsats till att redigera främmande data. Detta ger en tydligare ansvarsfördelning: Om tjänst A är trasig så är det tjänst A:s ansvar att åtgärda det och inte dess grannar.
Vid första anblicken, om allt är i ett team, varför komplicera saker med ett API? I verkligheten, även om du har en enda produkt uppdelad i moduler, kan en delad tabell leda till samma problem.
Till exempel är ordertjänsten ägare till ordertabellen och faktureringstjänsten kommer inte direkt åt den tabellen – den ringer till ordertjänstens slutpunkter för att få orderdetaljer eller för att markera en order som betald.
På en högre nivå, när två eller flera team ansvarar för olika områden, förblir principerna desamma. Till exempel:
Om Team B direkt frågar "Katalog"-tabellen som tillhör Team A, kan eventuella interna schemaändringar vid A (t.ex. lägga till fält, ändra struktur) påverka Team B.
Det korrekta tillvägagångssättet är att använda ett API: Team A tillhandahåller slutpunkter som GET /catalog/items
, GET /catalog/items/{id}
etc., och Team B använder dessa metoder. Om A kan stödja äldre och nyare versioner kan de släppa /v2, vilket ger B tid att migrera.
Med ett formellt kontrakt är alla ändringar synliga: i Swagger/OpenAPI, .proto-filer eller händelsedokumentation. Alla uppdateringar kan diskuteras i förväg, testas ordentligt och schemaläggas, med bakåtkompatibilitetsstrategier efter behov.
Förändringar i en tjänst har mindre inverkan på andra. Teamet behöver inte oroa sig för att "bryta" någon annan om de hanterar nya och gamla fält eller slutpunkter på rätt sätt, vilket säkerställer en smidig övergång.
API-gateways, autentisering och auktorisering (JWT, OAuth) är standard för tjänster, men nästan omöjliga med en delad tabell. Det är enklare att finjustera åtkomsten (vem kan anropa vilka metoder), föra loggar, spåra användningsstatistik och införa kvoter. Detta gör systemet säkrare och mer förutsägbart.
En delad tabell i databasen är en implementeringsdetalj snarare än ett avtal mellan tjänster och anses därför inte vara ett kontrakt. De många frågorna (komplex versionshantering, kaotiska förändringar, oklart ägande, säkerhet och prestandarisker) gör detta tillvägagångssätt ohållbart i längden.
Det korrekta tillvägagångssättet är Contract First vilket innebär att definiera interaktion genom formell design och följa principen att varje tjänst förblir ägaren till sina data. Detta bidrar inte bara till att minska tekniska skulder utan ökar också transparensen, snabbar upp produktutvecklingen och möjliggör säkra förändringar utan att behöva engagera sig i brandbekämpning över databasscheman.
Det är både en teknisk fråga (hur man designar och integrerar) och en organisatorisk fråga (hur team kommunicerar och hanterar förändringar). Om du vill att din produkt ska växa utan att behöva hantera oändliga nödsituationer när det gäller databasscheman, bör du börja tänka i termer av kontrakt snarare än direkt databasåtkomst.