Best practices sharing types across multiple Haskell packages/microservices

Viewed 272

I am working in a Haskell project that is composed of multiple microservices. There are certain types that are needed by multiple services at the same time and therefore are defined in separate libraries that are imported by whichever needs them (each type is versioned). But there are things about it that make me uncomfortable and I was wondering which are the best practises regarding that topic. In particular:

  • I don't know how to handle instances of shared types. In order to avoid orphans the instances must be defined in the type's module for each of the types. But then there are instances that are not needed by all the microservices that import the package and have to be included regardless, potentially adding redundant dependencies to the microservice. An example would be a datatype T that is shared by a frontend and a backend. It has an instance of Store in order to be stored in a database that is not needed by the frontend, but still makes the frontend depend of the store package.
  • In order to avoid the previous issue I could introduce a wrapper every time the type has to "leave" a microservice to represent that type going through the wire. In the previous example I would have a Storable T which would have an instance for Store. This adds quite a lot of overhead to the overall development process though.
  • Is it even a good idea to share the types or should each microservice define its own representation of each type, so nothing has to be shared? So for a type T a microservice MA would have a representation A.T and an instance to serialise it in order to send the data to a microservice B which would decode that data into a representation MB.T.

And finally, regarding the logistical side of the matter,

  • What's the most convenient way to structure the project? Would a monorepo be a better choice than having separate repos/submodules?
  • Maybe related to the previous one, is there any way to find out automatically which microservices should be redeployed when one of the shared libraries is modified because the changes in the library affect a piece of code that is being used by the microservice?
1 Answers

I know I'm spitting into the wind here, but since.

  1. these are your types for your microservices in your project;
  2. you want to separate instances from the type definitions only as a mechanism to separate concerns and not to "open" the type definition to random instances defined all over the place;
  3. you presumably aren't planning to release this as a general-purpose library for others where some miscreant would define an incompatible Store instance on one of your types;

then it seems reasonable to assume that your orphan instances -- despite being orphans -- can reasonably be taken as globally unique instances defined for those type/class combinations. So, this would be a justified use, if ever there was one, of -fno-warn-orphans.

The alternatives you mention for avoiding orphans (newtype wrappers and per-microservice type definitions) sound tiresome and error-prone, respectively, so much so that it seems preferable to keep the instances with the types despite the unnecessary dependencies.

Related