In polyglot microservice systems, most of the trouble comes from interface definitions.
You write IOrderService in C#, then rewrite it in Java, then in Python.
That is three files maintained in three places, and sooner or later one of them drifts from the others.
The bug surfaces at the system boundary, in production, when the two sides use
two different versions of the interface.
One source, and only one
UniContract solves this. You write the interface once, in a .contract file,
and the tool generates it for every target platform. Nobody syncs anything by hand; the generator does it,
so the platform versions cannot drift apart.
Here is a contract definition:
namespace DataStoring {
interface IDataStore {
readonly property Connection: IConnection
async method IsCollectionExists(name: string) => boolean
async method GetCollectionByName<T constraint IEntity>(name: string) => ICollection<T>
}
}
From this one definition you can generate a C# interface, a Java interface, and a Python abstract class. All three implement the same contract, and when the contract changes, all three change with it on the next generation run.
A compiler-like pipeline
The architecture follows that of a compiler. An ANTLR 4 based lexer and parser read
the .contract files, an element builder turns the syntax tree into a semantic model,
a semantic checker verifies the relationships, and finally pluggable emitters write
the code in the target language.
The emitters are Python modules, so anyone can write one without touching the core engine. A new platform (Rust, Go, TypeScript) needs only a new emitter module.
Design decisions
Constraint-based generics (<T constraint IEntity>)
keep type safety intact on every platform. This is harder than it sounds, because Java, C#, and Python
handle generics very differently; at the contract level, though, a generic has one meaning.
The async keyword becomes a Task in C#,
a Promise in TypeScript, and an async def in Python.
The contract says which methods are asynchronous, so you don't decide it per platform.
The import system lets contracts reference each other. PolyPersist, for example, takes all of its store interfaces from UniContract definitions, so the C# implementation and the other platforms implement the same interface.
Where do I use it?
Every cross-platform interface in PolyPersist is generated by UniContract. D3I uses it too, to keep the output of its emitters in agreement. Wherever the same contract has to be implemented on several platforms, I generate the code from it.