Type Driven Development: Anti-Corruption Layer

Written by

in

Part 15 – Anti-Corruption Layer

This is a series of posts I’m writing about using types as another tool in software development, Continuous Delivery, & keeping LLM’s honest. They’re also a design & refactoring tool, a communication tool, and reduce how many tests you have to write.

Parts
  1. Part 1 – Branded Types
  2. Part 2 – Product Types
  3. Part 3 – Union & Discriminated Unions
  4. Part 4 – Non-Empty Collections
  5. Part 5 – Indexed Types
  6. Part 6 – unknown vs any
  7. Part 7 – Result
  8. Part 8 – Schema
  9. Part 9 – Total Function
  10. Part 10 – Errors as Values
  11. Part 11 – Property Tests
  12. Part 12 – Type Proofs
  13. Part 13 – Exhaustiveness Checking
  14. Part 14 – Parse, Don’t Validate
  15. Part 15 – Anti-Corruption Layer
  16. Part 16 – Opaque Types
  17. Part 17 – Maybe
  18. Part 18 – Smart Constructors
  19. Part 19 – Pipeline
  20. Part 20 – Railway Oriented Programming
  21. Part 21 – Typestate
  22. Part 22 – Capabilities
  23. Part 23 – Immutability
  24. Part 24 – Making Impossible States Impossible
  25. Part 25 – Type Driven Development: How to do it
  26. Part 26 – Final Thoughts

An Anti-Corruption Layer turns someone else’s model into your own. Where unknown, schemas, & Parse, Don’t Validate convert untrusted data into trusted types, the ACL is sometimes a different layer that converts “their types to _our_ types”. Sometimes you can do this using the same Zod schema, other times it’s truly an entirely different domain boundary, shielding a legacy system away.

For example “Python devs don’t talk like us”. What Python devs call a “Person”:

person_dict = {
  'first_name': string,
  'last_name': string,
  'birthdate': '1980-02-06T12:44:53.063Z'
}Code language: JavaScript (javascript)

is NOT what we call a Person; firstName & lastName are camel cased, are 2 different Branded types, and birthday is a POSIX time with a Timezone vs. the whack ISO Date Strings.

person = {
  first: FirstName,
  last: LastName,
  birthday: { time: Time.Posix, zone: Time.Zone }
}

OOP devs call that “converting Data Transfer Objects (DTO) to Value Objects (VO)”.

The Domain angle is “We talk differently”; a legacy API talks about Products, still uses a hard to work w/Transactions list you have to search, & AccountType is a string with already known rules.

legacy = {
  // what accounts they have, usually just 1
  products: Array<number>,
  // a mix of credit and payments
  transactions: Array<Transaction>,
  // 'individual' or 'warehouse'
  accountTypes: Array<string>
}Code language: JavaScript (javascript)

Our team instead talks about different Customers with always at least 1 Account, transctions are either Credits or Payments, and the AccountType string (a known Union/Enum list) that dictate what kind of Customer it is, Individual or Warehouse:

type IndividualCustomer = {
  credits: Array<CreditTransaction>,
  payments: Array<PaymentTransaction>,
  firstAccount: Account,
  otherAccounts: Array<Account>
}

type WarehouseCustomer = { ... }Code language: HTML, XML (xml)

Often an ACL can just be a Schema and map function:

neverThrowPromise()
.andThen(safeParseJSON)
.andThen(legacySchema)
.andThen(mapToCustomer)Code language: CSS (css)

Other times it’s larger abstractions in a module mapping back and forth from legacy to modern, not just data but also behavior.

customerToLegacy
|> legacyToJSON
|> legacySave

The key is, like how “Parse, Don’t Validate” ensures you don’t have their bad data model in your code causing Shotgun Parsing, you don’t want legacy concepts/words to “infect” your Domain, so you use an ACL at the boundary. Other times it’s just different systems use different words and concepts than your team does and it causes confusion & communication problems. It’s easier if your team uses the same language and words, and puts that into the code, then translate to/from others.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *