Skip to content
This repository has been archived by the owner on Apr 13, 2019. It is now read-only.

schema overview

Konstantin Sobolev edited this page Apr 1, 2016 · 15 revisions

Domain model

Almost every system design starts with defining domain models, what do they consist of and how they relate to each other. This is the language PMs and customers understand and it allows to set up universal dictionary for interfaces and implementations. So lets start with a few abstract entities:

Every entity can be described by multiple representations: ID, URI, record or even binary data like images. We can capture this concept by introducing multi-types, collections of types that can act as different representations of the same entity.

Multi-types

User is a multi-type that can be either UserId or UserRecord, File can be either FileId or FileRecord and so on.

Now we have records, fields and field types. FileRecord.name is probably a string, but what’s the type of FileRecord.owner? Before deciding on it lets step back and think about the nature of a field. Fields define relationships between entities and field names hint at their semantics. We don’t care about implementation details at this point, all we want to say is that a File has an owner which is a User

owner field can hold any representation of a User, and so it's type should naturally be User. But what happens when a client asks to fetch a file record with name and owner fields without specifying which representation of the owner to build? Should we fail or build a random one? This uncertainty can be solved by adding one more property to the multi-type, a default type.

todo: default type should also be overridable on a per-field basis, either on the schema or IDL level. Update this doc when refined

So now we can say that User is a multi-type which can be either a UserId or a UserRecord, and by default it is going to be a UserId

Multi-type is a collection of types that can act as different representations of the same entity type. This is not always an "is-a" relationship, so we can't model it as inheritance, this is more of "can-be".

Instances of multi-types are called multi-values and are modelled as records, with field names being type aliases. For example FileRecord.owner of type User can contain

{
  'userId' : data
}

or

{
  'userRecord' : data
}

or even both at the same time:

{
  'userId' : data,
  'userRecord' : data
}

Fields that have multi-types are called pivotable.

This feature will be used by the federator, but it also helps to deal with backwards-incompatible changes in the models. Imagine File.owner was erroneously typed as UserId disallowing any representations other than UserId. Changing it’s type to UserRecord would be an incompatible change, but making it a User with default model of UserId won’t break any existing clients but will make it pivotable.

Semantic types

Imagine that UserId, FileId and EnterpriseId are just long values. It is possible to simply use long type instead, but creating special-purpose types has a number of benefits:

  • Clarity. Meaning of the value follows from it's type
  • Safety. You can add up two long values, but adding UserId to FileId would make no sense
  • Computers can reason about it too. If there is an automatic conversion from UserId to UserRecord then we can try to apply it to a given UserId instance. We couldn't do the same for an arbitrary long value.

Such special-purpose types are called sematic types, and usually they are based on one of the primitive types.

Supported types

Framework supports the following type kinds:

  • Primitives. Both built-in ones like long or string and user-defined ones like UserId. User-defined primitive types must be based on one of the built-it primitive types
  • Records. As usual, records have named fields, fields have types and can be pivotable. Records can inherit from another records, which means that they get all of the fields from the parents and their instances can be used where parent instances are expected. In other words this is a real 'is-a' relationship
  • Unions. Like records with tags instead of the fields. Union instances can only have exactly one field populated. Unions do not support inheritance.
  • Maps with typed keys and values. Values can't be multi-typed, entries can
  • Lists with typed values
  • Multi-types

Inheritance is only allowed between record types. Multiple inheritance is supported.

Metadata

Any user-defined type can have metadata type associated with it. If such association is defined then every instance of this type will have a (possibly empty) metadata instance associated with it.

As an example imagine we're modelling Users service and one of the actions returns a list of users. This list can be very big, so we need pagination. Since framework doesn't provide any built-in support for pagination we will have to model it ourselves, and metadata is the right mechanism for describing pagination context:

{
  'list': {
    'name': 'UserList',
    'elementType': 'User',
    'meta': 'OffsetBasedPagingInfo'
  },

  'record': {
    'name': 'OffsetBasedPagingInfo',
    'fields': [
      {
        'name': 'start',
        'type': 'long'
      },
      {
        'name': 'offset',
        'type': 'long'
      }
    ]
  }
}

Now every instance of UserList will have an optional paging info metadata associated with it.

Clone this wiki locally