You are on page 1of 11

A Domain-Specific Language for Microservices

Jacob Donham
jdonham@twitter.com
Twitter Inc.
San Francisco, CA, United States
Abstract (e.g. from a user’s browser) involves calling tens of different
A common architectural pattern for complex online systems services and hundreds of service instances.
is a collection of “microservices” communicating via RPC in- This arrangement is driven by the following question:
terfaces. A service architecture provides autonomy for teams With hundreds of engineers working on one sys-
to develop, deploy, and operate system modules without cen- tem, how does anyone get anything done?
tralized coordination. But this autonomy comes at a cost in
leverage—the fixed costs of running a service are high, and From this point of view, a services architecture is appealing
the desire to avoid them can lead to poor design choices. because it gives engineering teams autonomy:
In this paper we describe Strato, a system built at Twitter • each service has its own code base, so teams may de-
that runs many microservices hosted in a shared service velop them without central coordination;
platform—trading a little autonomy for a lot of leverage. • each service is independently deployed, so teams may
Hosted microservices are written in a Scala-like DSL that operate them without central coordination; and
supports transparent concurrency and native access to Thrift • each service has an owning team, so domain knowl-
data (the common currency of services at Twitter). The DSL edge and responsibility are focused.
compiles to a concurrency library written in Scala with an
arrows-based interface. We detail the design and implemen- However, services have high fixed costs—common costs
tation of the DSL and concurrency library, and evaluate the to develop, maintain, and operate each service, independent
effectiveness of Strato in improving leverage at Twitter. of the application logic it contains. For example:
CCS Concepts • Applied computing → Service-oriented • setting up the RPC service framework;
architectures; • Software and its engineering → Func- • continuous integration and deployment;
tional languages; Concurrent programming structures; • monitoring, alerting, logging, and tracing;
Domain specific languages; • authorization and access control;
• scale and capacity management; and
Keywords Scala, microservices • operations and on-call.
ACM Reference Format:
At Twitter there are many sophisticated libraries, frame-
Jacob Donham. 2018. A Domain-Specific Language for Microser-
vices. In Proceedings of the 9th ACM SIGPLAN International Scala
works, and subsystems to address these needs; still it is up
Symposium (Scala ’18), September 28, 2018, St. Louis, MO, USA. ACM, to each service owner to assemble the pieces into a function-
New York, NY, USA, 11 pages. https://doi.org/10.1145/3241653.3241654 ing service, and to maintain and operate it over time. These
costs reduce teams’ leverage—that is, productivity gained by
reusing systems or capabilities.
1 Introduction
Often the application logic of a service is modest and does
Like many other large, complex online systems, the Twit- not justify this overhead; it’s therefore pragmatic to build
ter consumer product is implemented as a constellation of new functionality into an existing service rather than bring
independent services communicating via Thrift [1] RPC in- up a new one. As more and more functionality is built into a
terfaces. There are many hundreds of services running in service, it becomes a “monolith”, obviating the advantages of
production at Twitter; handling a typical top-level request a service architecture: a monolithic service must be centrally
developed, deployed, and operated; and ownership is diffuse.
Permission to make digital or hard copies of part or all of this work for
personal or classroom use is granted without fee provided that copies are Teams working in a monolithic service enjoy greater leverage
not made or distributed for profit or commercial advantage and that copies by reusing its facilities, but lose a lot of autonomy.
bear this notice and the full citation on the first page. Copyrights for third- In order to improve engineering teams’ leverage without
party components of this work must be honored. For all other uses, contact taking too much of their autonomy, Twitter is building a new
the owner/author(s).
system, called Strato, which runs many microservices in a
Scala ’18, September 28, 2018, St. Louis, MO, USA
shared service platform. The Strato team is responsible for
© 2018 Copyright held by the owner/author(s).
ACM ISBN 978-1-4503-5836-1/18/09. most of the fixed costs of developing and owning a service,
https://doi.org/10.1145/3241653.3241654 while application teams are responsible for developing their

2
Scala ’18, September 28, 2018, St. Louis, MO, USA Jacob Donham

application logic. Teams write application logic in an (exter- Thrift IDL as part of configuration deployment. The collec-
nal, rather than embedded) domain-specific language called tion of available operations and their types is organized into
StratoQL. a catalog, analogous to the system catalog in a SQL database.
StratoQL is a Scala-like language with some unusual fea-
tures to support the domain of microservices at Twitter: 3 StratoQL
transparent concurrency to support calling other services
without syntactic overhead, and a structural type system to userByName.fetch("jack") match {
support native access to Thrift data. case { userId = uId } =>
The contributions of this paper are the following: for {
{ tweetId = tId, authorId = aId } <-
• a description of StratoQL’s type system and typecheck- timelineByUserId.fetch(uId)
{ text = text } = tweetById.fetch(tId)
ing algorithm (Section 3)
{ userName = userName } = userById.fetch(aId)
• a description of a concurrency library based on arrows
} yield (userName, text)
[8] called Stitch, and how StratoQL is compiled to this }
library (Section 4)
• a report on how well Strato has achieved the goal of
improving engineering teams’ leverage without taking Figure 1. StratoQL
too much of their autonomy (Section 5)
See Figure 1 for a representative StratoQL expression,
which fetches a user record by username, fetches the user’s
2 The Strato Service Platform Twitter timeline, then for each entry in the timeline fetches
the tweet text and the author’s username.
For context we briefly sketch the Strato service platform:
In the for-expression over timeline entries, the body ex-
Like most services at Twitter, Strato is written in Scala
pression is run for all elements concurrently, so the individ-
and uses the Finagle library [16] to implement its Thrift RPC
ual calls to the tweetById and userById microservices are
client and server stacks; it runs in Twitter’s datacenters on
run concurrently, and are batched into a single RPC call to
top of Aurora/Mesos [3] [4]; it uses various homegrown and
each underlying service (see Section 4). The syntax
open-source libraries and systems for continuous integration,
{ userId = uId } is a record pattern (as found in OCaml
deployment, monitoring, alerting, logging, tracing, and so
and other languages), matching the userId field of a record
on; and we typically deploy the system twice a week.
value and binding its value to uId; the record may have
Unlike most services at Twitter, the Strato codebase con-
additional unmentioned fields.
tains no application logic: microservices running in Strato
StratoQL has a Scala-like syntax (and a similar approach
are defined by configuration files, which are deployed sepa-
to typechecking, see Section 3.3), but differs from Scala in
rately from the Strato service. Configuration deployment is
several ways:
a lightweight process compared to service deployment: we
In contrast to Scala’s nominal type system, StratoQL’s type
typically deploy configuration at least daily and sometimes
system is purely structural. Structural typing is a good fit for
several times a day, and we are working toward continuous
our intended domain: because StratoQL code deals mostly
configuration deployment.
with external data (from storage or other services), types
Microservices in Strato may be configured using com-
must be meaningful across different services and different
ponents that capture common patterns of infrastructure at
versions of the same service. For a similar reason, Thrift
Twitter, such as accessing data in Twitter’s storage layer,
RPC interfaces are effectively structurally-typed: there is no
Manhattan [15], or calling external Thrift services. Compo-
requirement that a client and server agree exactly on their
nents may be parameterized with StratoQL expressions (for
interface (when they are deployed separately it is common
example, to validate a data structure before writing it to stor-
that they do not agree) so long as the different interfaces are
age); or entire microservices may be defined with StratoQL
wire-compatible—that is, all serialized values of one interface
(for example, to call several other microservices and manipu-
are valid serialized values of another. Thrift’s serialization
late the results). StratoQL is also used as a syntax to express
format is designed so that wire-compatibility is similar to
configuration (it is run at configuration loading time).
structural subtyping—for example it permits a wider record
Operations on microservices are described by types (see
to be passed where a narrower one is expected.
Figure 3.1). These types are used to generate Thrift, REST,
StratoQL provides native access to Thrift data: Thrift types
and GraphQL [7] interfaces for accessing microservices, and
can be represented directly as Strato types (in particular
an internal web interface for browsing them. Because Strato
structs become records and unions/enums become vari-
must interoperate with external systems that use type defi-
ants), and StratoQL provides syntax for constructing and
nitions written in Thrift IDL, we generate Strato types from
pattern-matching such types. Structural typing avoids the

3
A Domain-Specific Language for Microservices Scala ’18, September 28, 2018, St. Louis, MO, USA

need to import or refer to external Thrift type definitions binary serialization, or a default value to use when a field is
in application code; developers can write down expressions missing at deserialization time.
of the desired type directly. Together these features make it
very convenient to work with Thrift data in StratoQL. In con- 3.2 Syntax
trast, working with Thrift data from Scala can be somewhat
awkward. Scrooge [17], Twitter’s tool for generating Scala
bindings from Thrift IDL, generates Scala case classes and e ::= x
sealed traits from Thrift structs and unions. Scala doesn’t | () | true | 7 | "foo" | . . .
support pattern matching by field name in case classes, so | (e 1 , . . . , en )
decomposing large, complicated Thrift structures is more | { l 1 = e 1 , . . . , ln = en }
difficult. And requiring imports and namespacing makes | C | C(e)
application code noisier. | Seq(e 1 , . . . , en ) | Set(e 1 , . . . , en )
Another departure from Scala is that concurrency in Stra- | Map(ek 1 -> ev1 , . . . , ekn -> evn )
toQL is implicit; for example in an expression (e 1 , e 2 ), e 1 and | { (x 1 : τ1 , . . . , x n : τn ) => e }
e 2 are evaluated concurrently. This makes it convenient to | { (x 1 , . . . , x n ) => e }
compose calls to several services in a direct style, in con- | e 1 + e 2 | e 1 == e 2 | . . .
trast to concurrency libraries with monadic or applicative | e.l
interfaces such as Twitter’s Future [6]. | e.m(e 1 , . . . , en )
Finally, StratoQL is much simpler than Scala: it does not | e.m[τ1 , . . . , τm ](e 1 , . . . , en )
support any of the object-oriented or modular aspects of | val p = ep ; e
Scala or any of Scala’s fancier type-system features. For our | e match { case p1 => e 1 . . . case pn => en }
target domain we don’t expect users to write very large or | for { x 1 <- e 1 . . . } yield e
complicated programs; and we have little need to interoper-
ate with JVM code, so it is not necessary to support the full p ::= x | _
range of JVM types. | () | true | 7 | "foo" | . . .
| (p1 , . . . , pn )
3.1 Types | { l 1 = p 1 , . . . , l n = pn }
| C | C(p)

τ ::= x
Figure 3. Expressions
| Unit | Byte | Boolean | Int | Double | String | . . .
| (τ1 , . . . , τn )
| { l 1 : τ 1 , . . . , l n : τn } See figure 3 for StratoQL syntax, comprising primitive
| ⟨ C 1 (τ1 ) | . . . | Cn (τn ) ⟩ values, tuples, records, variant arms, collections, functions
| Option[τ ] | Seq[τ ] | Set[τ ] | Map[τk , τv ] (with and without argument type annotations), arithmetic
| µx .τ and logical operators, field projections and method appli-
| X cations (with and without type parameter instantiations),
| Any binding, pattern matching, and for-expressions.
| Nothing
| ∀X 1 , . . . , Xm .(τ1 , . . . , τn ) → τ 3.3 Typechecking
We follow Scala in implementing Pierce and Turner’s local
Figure 2. Types type inference [13], which involves three main algorithms:
• synthesizing a type from an expression when we don’t
Strato types (see Figure 2) that represent concrete, serial- know what type to expect;
izeable data are similar to Thrift types: they include primitive • checking an expression against an expected type; and
types, tuples, records (similar to Thrift structs), variants • subtyping when synthesis meets checking.
(subsuming Thrift unions and enums), a fixed set of container as well as an algorithm to determine type parameters in
types (Option, Seq, Set, and Map) and recursive types. applications of parameterized functions by solving local con-
In addition Strato supports type parameters, top and bot- straints.
tom elements, and type-parameterized functions; these types To implement structural subtyping with recursive types
are used in typechecking but do not represent concrete, seri- we follow Amadio and Cardelli [2]: we maintain a context of
alizeable data. subtyping assumptions (Amadio and Cardelli call it a “trail”)
Types are enriched with annotations that control special involving recursive type bindings, in order to avoid looping
treatment in particular settings; e.g. the Thrift field tag for when traversing recursive types (trail in Figure 6).

4
Scala ’18, September 28, 2018, St. Louis, MO, USA Jacob Donham

vare val
→ ← →
Γe (x) = τ Γe ⊢ ep ∈ τp ⇒ ep′ · ⊢ p ∈ τp ⇒ Γp Γe + Γp ⊢ e ∈ τ ⇒ e ′
→ →
Γe ⊢ x ∈ τ ⇒ x Γe ⊢ (val p = ep ; e) ∈ τ ⇒ (val p = ep′ ; e ′)

variant record
→ → →
Γe ⊢ e ∈ τ ⇒ e ′ Γe ⊢ e 1 ∈ τ1 ⇒ e 1′ ... Γe ⊢ en ∈ τn ⇒ en′
→ →
Γe ⊢ C(e) ∈ ⟨ C(τ ) ⟩ ⇒ C(e ′) Γe ⊢ { l 1 = e 1 , . . . , ln = en } ∈ { l 1 : τ1 , . . . , ln : τn } ⇒ { l 1 = e 1′ , . . . , ln = en′ }
apply
← ←
Γe (f ) = ∀X 1 , . . . , Xm .(τ1 , . . . , τn ) → τ ·; Γe ; · ⊢ e 1 ∈ τ1 ⇒ e 1′ ; θ 1 ... θ n−1 ; Γe ; · ⊢ en ∈ τn ⇒ en′ ; θ n σ = σθ τ

Γe ⊢ f (e 1 , . . . , en ) ∈ στ ⇒ σ (f [X 1 , . . . , Xm ](e 1′ , . . . , en′ ))

Figure 4. Synthesis

We follow Odersky [12] in extending type argument syn- variant


thesis to handle unannotated method arguments (e.g. synthesize a type for a variant expression by synthe-
seq.map { x => x + 1 }), by checking function arguments sizing a type for the associated expression.
under a set of type parameter constraints (apply in Figure record
4) rather than synthesizing function arguments and solving synthesize a type for a record expression by synthesiz-
constraints at the point of application. ing a type for each field.
Our algorithm differs from these forebears in two ways: apply
• we compute an asynchrony effect for expressions (see synthesize a type for a polymorphic function appli-
Section 3.5), and support polymorphic asynchrony and cation by checking argument expressions to generate
asynchrony inference in function application; and type parameter constraints, then instantiating con-
• we extend the checking algorithm to encompass type straints θ to get a type parameter substitution σ (fol-
conversion when an expression is not a subtype of the lowing Section 3.5 of Pierce and Turner [13]).
expected type, but may be usefully converted to the and also structural rules (tuple, seq, etc.) and rules for prim-
expected type (see Section 3.4). itives and operators (not shown). Types of microservice op-
3.3.1 Synthesis erations are looked up in the catalog.
The synthesis algorithm
3.3.2 Checking

Γe ⊢ e ∈ τ ⇒ e ′ The checking algorithm
relates

• an expression e, which is elaborated to e ′ with missing θ ; Γe ; Γτ ⊢ e ∈ τ ⇒ e ′; θ ′
types filled in;
• a type τ synthesized from e; and relates
• a context Γe binding expression variables in e. • an expression e, which is elaborated to e ′ with missing
with rules (see Figure 4) types filled in;
vare • a type τ against which e is checked;
synthesize a type for a variable by looking it up in the • contexts Γe and Γτ binding expression variables in e
environment. and type variables in τ respectively; and
val • type parameter constraints θ , which may be updated
synthesize a type for a val binding by synthesizing to θ ′.
a type for the bound expression ep then synthesiz-
with rules (see Figure 5)
ing a type for the body e in an environment enriched
with the pattern bindings. An auxiliary algorithm Γ ⊢ rec

p ∈ τ ⇒ Γ ′ (not shown) is used to check the pattern check an expression against a recursive type by adding
against the synthesized type and extract the binding the type variable to the type environment then check-
types. The match rule is similar (not shown). ing against the body.

5
A Domain-Specific Language for Microservices Scala ’18, September 28, 2018, St. Louis, MO, USA

rec varτ
← ←
θ ; Γe ; Γτ + (x : µx .τ ) ⊢ e ∈ τ ⇒ e ; θ ′ ′
Γτ (x) = τ θ ; Γe ; Γτ ⊢ e ∈ τ ⇒ e ′; θ ′
← ←
θ ; Γe ; Γτ ⊢ e ∈ µx .τ ⇒ e ′; θ ′ θ ; Γe ; Γτ ⊢ e ∈ x ⇒ e ′; θ ′
variant

θ ; Γe ; Γτ ⊢ e ∈ τk ⇒ e ′; θ ′

θ ; Γe ; Γτ ⊢ Ck (e) ∈ ⟨ Ck (τk ) | . . . ⟩ ⇒ Ck (e ′); θ ′
record
← ←
θ ; Γe ; Γτ ⊢ e 1 ∈ τ1 ⇒ e 1′ ; θ 1 ... θ n−1 ; Γe ; Γτ ⊢ en ∈ τn ⇒ en′ ; θ n

θ ; Γe ; Γτ ⊢ { l 1 = e 1 , . . . , ln = en , . . . } ∈ { l 1 : τ1 , . . . , ln : τn } ⇒ { l 1 = e 1′ , . . . , ln = en′ }; θ n
function

θ ; Γe + (x 1 : τ1 , . . . , x n : τn ); Γτ ⊢ e ∈ τ ⇒ e ′; θ ′

θ ; Γe ; Γτ ⊢ { (x 1 , . . . , x n ) => e } ∈ (τ1 , . . . , τn ) → τ ⇒ { (x 1 : τ1 , . . . , x n : τn ) => e ′ }; θ ′
val
→ ← ←
Γe ⊢ ep ∈ τp ⇒ ep′ · ⊢ p ∈ τp ⇒ Γp θ ; Γe + Γp ; Γτ ⊢ e ∈ τ ⇒ e ′; θ ′

θ ; Γe ; Γτ ⊢ (val p = ep ; e) ∈ τ ⇒ (val p = ep′ ; e ′); θ ′

synth

Γe ⊢ e ∈ τs ⇒ e ′ θ ; ·; Γτ ; Σ ⊢ τs <: τ ⇒ θ ′

θ ; Γe ; Γτ ⊢ e ∈ τ ⇒ e ′; θ ′

Figure 5. Checking

varτ environment enriched with the pattern bindings. The


check an expression against a type variable by check- match rule is similar (not shown).
ing against the variable’s binding in the type environ- synth
ment. (if no other rule applies) check an expression against
variant a type by synthesizing a type for the expression then
check a variant expression against a variant type by checking that the synthesized type is a subtype of the
finding the corresponding label in the type and check- expected type. (In particular this rule applies when
ing the associated expression against the associated checking against a type parameter, so the synthesized
type; extra arms in the type are ignored. type constrains the parameter.)
record and also structural rules (tuple, seq, etc.) and rules for prim-
check a record expression against a record type by itives (not shown).
checking each field in the expression against the corre-
sponding field in the type; extra fields in the expression 3.3.3 Subtyping
are ignored. The subtyping algorithm
function
check a function expression without argument type θ ; Γa ; Γb ; Σ ⊢ τa <: τb ⇒ θ ′
annotations against a function type by checking a type
relates
for the function body in an environment enriched with
the argument type bindings; elaborate the expression • types τa and τb ;
with argument type annotations. • contexts Γa and Γb binding type variables in τa and τb
val respectively;
check a val binding by synthesizing a type for the • a context Σ of subtyping assumptions τa <: τb ; and
bound expression ep then checking the body e in an • type parameter constraints θ , which may be updated
to θ ′.

6
Scala ’18, September 28, 2018, St. Louis, MO, USA Jacob Donham

trail parama
(τa <: τb ) ∈ θ θ ⊢ X <: τb ⇒ θ ′
θ ; Γa ; Γb ; Σ ⊢ τa <: τb ⇒ θ θ ; Γa ; Γb ; Σ ⊢ X <: τb ⇒ θ ′

reca vara
θ ; Γa + (x : µx .τa ); Γb ; Σ + (µx .τa <: τb ) ⊢ τa <: τb ⇒ θ ′ Γa (x) = τa θ ; Γa ; Γb ; Σ ⊢ τa <: τb ⇒ θ ′
θ ; Γa ; Γb ; Σ ⊢ µx .τa <: τb ⇒ θ ′ θ ; Γa ; Γb ; Σ ⊢ x <: τb ⇒ θ ′
variant
θ ; Γa ; Γb ; Σ ⊢ τa1 <: τb1 ⇒ θ 1 ... θ n−1 ; Γa ; Γb ; Σ ⊢ τan <: τbn ⇒ θ n
θ ; Γa ; Γb ; Σ ⊢ ⟨ C 1 (τa1 ) | . . . | Cn (τan ) ⟩ <: ⟨ C 1 (τb1 ) | . . . | Cn (τbn ) | . . . ⟩ ⇒ θ n
record
θ ; Γa ; Γb ; Σ ⊢ τa1 <: τb1 ⇒ θ 1 ... θ n−1 ; Γa ; Γb ; Σ ⊢ τan <: τbn ⇒ θ n
θ ; Γa ; Γb ; Σ ⊢ { l 1 : τa1 , . . . , ln : τan , . . . } <: { l 1 : τb1 , . . . , ln : τbn } ⇒ θ n

Figure 6. Subtyping

with rules (see Figure 6) 3.4 Conversion


trail A convenient feature of Thrift IDL is that it supports default
use subtyping assumptions in the trail to cut off recur- values for struct fields. Default values provide for wire-
sion. trail takes precendence over all other rules. compatibility: a struct extended with a field that has a
parama default value is wire-compatible with the original struct
update a type parameter constraint; there is a symmet- in both directions—a receiver expecting the original struct
ric paramb (not shown). It is an invariant that type can ignore the extra field, while a receiver expecting the
parameters do not appear on both sides of <: at once. extended struct can fill in the default value. It’s common
reca for Thrift structs to have many fields with default values,
handle a recursive type by adding its bound variable reflecting their evolution over time. We support default field
to the type environment, and adding the current pair values in Strato by annotating field types with a default
of types to the trail; there is a symmetric recb (not value1 .
shown). Record field defaults are also useful in application code;
vara they are supported in Scala with argument defaults on case
handle a type variable by consulting the environment; class constructors. In StratoQL records are not constructed
there is a symmetric varb (not shown). with named constructors; however we can achieve much
variant the same effect through conversion: when checking a record
handle variants by checking subtyping for the types against a type, if the record lacks a field that is present in
associated with each label; also permit a narrower the type, we convert the record to the expected type, filling
variant where a wider one is expected. in the default.
record By the same mechanism we convert narrower to wider
handle records by checking subtyping for the types numeric types. We also do conversions that make it easier
associated with each label; also permit a wider record to update the types of configuration objects (since code and
where a narrower one is expected. configuration are deployed separately there is a need for
backward compatibility). For example a variant expression
and also structural rules (tuple, seq, etc.) and rules for prim- Foo may be converted to type ⟨ Foo({ bar: Boolean = true }) ⟩
itives (not shown). by filling in the default value of the record.
Type parameter constraint sets θ consist of upper and
lower bounds τa <: X <: τb for a set of type parameters.
Updating the upper bound of a parameter θ ⊢ X <: τb ′ ⇒ θ ′
computes the least upper bound of the previous upper bound
τb and the new upper bound τb ′ (and similarly for lower
1 Thrift
bounds); update fails if the resulting constraint is unsatisfi- IDL also supports optional fields; Strato conflates them to fields of
able. Option type with default None.

7
A Domain-Specific Language for Microservices Scala ’18, September 28, 2018, St. Louis, MO, USA

variant synthconvert

θ ; Γa ; Γb ; Σ ⊢ { } <: e ⇒ θ ′ Int { Long Γe ⊢ e ∈ τs ⇒ e ′ θ ; ·; Γτ ; Σ ⊢ τs <: τ ⇒ θ ′
{ {
θ ; Γa ; Γb ; Σ ⊢ C <: C(e) ⇒ θ ′ θ ; Γa ; Γb ; Σ ⊢ Int <: Long ⇒ θ ′ ←
θ ; Γe ; Γτ ⊢ e ∈ τ ⇒ convertτs ,τ (e ′); θ ′
{ {

Figure 7. Conversion

tuple← tuple→
← ← → →
θ ; Γe ; Γτ ⊢ e 1 ∈ τ1 ⇒ e 1′ ; θ ′ ... θ ; Γe ; Γτ ⊢ en ∈ τn ⇒ en′ ; θ ′ Γe ⊢ e 1 ∈ τ1 ⇒ e 1′ ... Γe ⊢ en ∈ τn ⇒ en′
α α α1 αn
← →
θ ; Γe ; Γτ ⊢ (e 1 , . . . , en ) ∈ (τ1 , . . . , τn ) ⇒ (e 1′ , . . . , en′ ); θ ′ Γe ⊢ (e 1 , . . . , en ) ∈ (τ1 , Ô. . . , τn ) ⇒ (e 1′ , . . . , en′ )
α i αi

function

θ ; Γe + (x 1 : τ1 , . . . , x n : τn ); Γτ ⊢ e ∈ τ ⇒ e ′; θ ′
αf

θ ; Γe ; Γτ ⊢ { (x 1 , . . . , x n ) => e } ∈ (τ1 , . . . , τn ) → τ ⇒ { (x 1 : τ1 , . . . , x n : τn ) => e ′ }; θ ′
αf
α

apply
Γe (f ) = ∀A.∀X 1 , . . . , Xm .(τ1 , . . . , τn ) → τ
A
← ←
·; Γe ; · ⊢ e 1 ∈ τ1 ⇒ e 1′ ; θ 1 ... θ n−1 ; Γe ; · ⊢ en ∈ τn ⇒ en′ ; θ n σ = σθ τ
A A

Γe ⊢ f (e 1 , . . . , en ) ∈ στ ⇒ σ (f [X 1 , . . . , Xm ](e 1′ , . . . , en′ ))
σA

synth

Γe ⊢ e ∈ τs ⇒ e ′ θ ; ·; Γτ ; Σ ⊢ τs <: τ ⇒ θ ′ θ ′ ⊢ α s <: α ⇒ θ ′′
αs

θ ; Γe ; Γτ ⊢ e ∈ τ ⇒ e ′; θ ′′
α

Figure 8. Asynchrony

The subtyping and checking algorithms extend naturally be synchronous (because they compile to choose calls, see
to support conversion (see Figure 7). We extend the expres- Section 4.2); and when compiling an expression we could
sion syntax with an element convertτ ,τ ′ indicating conver- save some overhead if we know it’s synchronous (we do not
sion, and elaborate expressions using this element when con- currently implement this optimization).
version is needed during typechecking (in the synthconvert To determine asynchrony of StratoQL expressions, we
rule). The convertibility algorithm extend the synthesis and checking algorithms to compute an
asynchrony effect [11] α along with the type. Function types
θ ; Γa ; Γb ; Σ ⊢ τa <: τb ⇒ θ ′
{ carry an asynchrony flag to indicate that an application of
extends the subtyping algorithm with rules for various con- the function has the given asynchrony.
versions; Σ becomes a context of convertibility rather than There is a natural notion of sub-asynchrony: a synchro-
subtyping assumptions. nous expression may appear where an asynchronous expres-
sion is expected (but not the other way around); similarly a
3.5 Asynchrony function of synchronous type may appear where a function
Because concurrency in StratoQL is implicit, whether a Stra- of asynchronous type is expected.
toQL expression is asynchronous is not revealed by its type, For applications of higher-order functions, we’d like to
in contrast to concurrency libraries with monadic or applica- consider the asynchrony of the argument functions, so e.g.
tive interfaces [6] [10]. map can be used in a synchronous context if its argument
However it is often useful to know whether an expression function has synchronous type. Therefore we implement
is asynchronous. For example, Strato configuration files must asynchrony polymorphism (for a single parameter A) and
be synchronous; in match expressions pattern guards must

8
Scala ’18, September 28, 2018, St. Louis, MO, USA Jacob Donham

inference as for types; we track constraints in θ and extend to compose larger computations. We represent a computa-
sub-asynchrony to constrain A. tion as a syntax tree (e.g. a join becomes a Join node); to
The rule modifications are mostly straightforward (see evaluate a computation we
Figure 8): • traverse the syntax tree to find atomic calls;
tuple← • deduplicate and batch the atomic calls;
check a tuple against α by checking each component • invoke underlying services with batches of calls;
against α. • wait for some underlying service RPC to return;
tuple→ • substitute the results of calls into the tree;
synthesize α for a tuple by synthesizing α i for each • simplify the tree; and
component, returning true if any α i is true. • repeat until the tree represents a constant value.
function Stitch predates Strato by a few years; it’s used in tens of
check the function body against α f (asynchrony of the existing services at Twitter.
function type); a function literal is synchronous so α
is ignored. 4.1 Stitch Arrows
apply
synthesize α for an asynchrony-polymorphic func- sealed trait Arrow[-T, +U]
tion application by checking argument expressions
to generate asynchrony parameter constraints, and def identity[T]: Arrow[T, T]
instantiate them as for types. Types τi may contain def value[T](v: T): Arrow[Any, T]
the asynchrony parameter, which is constrained by def call[T, U](s: Service[T, U]): Arrow[T, U]
def map[T, U](f: T => U): Arrow[T, U]
checking a function against τi .
synth
def join[T, U, W](a1: Arrow[T, U], a2: Arrow[T, W]):
check α for an expression by synthesizing α s for the Arrow[T, (U, W)]
expression then checking that α s is a sub-asynchrony
of α; the sub-asynchrony check θ ′ ⊢ α s <: α ⇒ θ ′′ def andThen[T, U, W](a1: Arrow[T, U], a2: Arrow[U, W]):
may update constraints on the asynchrony parameter Arrow[T, W]
as with type parameters.
def choose[T, U, V](
4 Stitch choices: (PartialFunction[T, U], Arrow[U, V])*
): Arrow[T, V]
// Haxl
friendsOf :: UserId -> Fetch [UserId] def sequence[T, U](a: Arrow[T, U]):
Arrow[Seq[T], Seq[U]]
length <$> intersect' (friendsOf x) (friendsOf y)
where intersect' = liftA2 intersect def traverse[T, U](a: Arrow[T, U], ts: Seq[T]):
Stitch[Seq[U]]
// Stitch
def friendsOf(id: UserId): Stitch[Seq[UserId]]
Figure 10. Arrows
join(friendsOf(x), friendsOf(y))
.map { case (a, b) => After some experience using Stitch in production systems,
a.intersect(b).length we discovered that its monadic / applicative interface has a
} performance cost:
A typical computation involves several flatMaps to cap-
Figure 9. Stitch ture data dependencies between service calls. To evaluate
a flatMap we run a Stitch[T]-returning function, which
In order to support StratoQL’s implicit concurrency, we constructs a syntax tree for the subcomputation. This in-
compile StratoQL expressions into calls to a Scala concur- terface permits subcomputations that depend dynamically
rency library called Stitch. Stitch was inspired by Facebook’s on data fetched earlier in the computation. However, this
Haxl library [10], and supports a similar monadic / applica- is relatively rare in practice; most of the time a flatMap
tive interface. For example, Figure 9 shows a translation of always builds a tree of the same shape, with data dependen-
an example from Marlow [10]; here join(a, b) is roughly cies substituted into the tree in a uniform way. (For example
(,) <$> a <*> b in Haskell terms. the computation in Figure 1 is static in this way.) Rebuild-
Like Haxl, Stitch provides atomic computations that rep- ing nearly-identical trees over and over is costly in memory
resent calls to underlying services, and a set of combinators allocation and CPU time.

9
A Domain-Specific Language for Microservices Scala ’18, September 28, 2018, St. Louis, MO, USA

To improve performance we turned to arrows [8]. An in- the pattern and guard are satisfied and updates the value
terface based on arrows gives us a way to build the tree environment with the extracted bindings. Because these are
representing a computation once, then evaluate it many ordinary Scala partial functions, they must be synchronous,
times with different data. An Arrow[T, U] is similar to a so we check that StratoQL expressions in pattern guards are
T => Stitch[U], but the static structure of the computation it synchronous (see Section 3.5).
represents is visible to the Stitch implementation. So the im-
plementation can avoid needless tree allocation, and perform 4.3 Example
other optimizations (e.g. folding constant arrows).
See Figure 10 for a sketch of the interface, which includes def lookup(id: String) = map { env => env(id) }
combinators def pairWithArg(a: Arrow[T, U]) = join(identity, a)
identity
return input unchanged val userByNameFetch = call(...)
val timelineByUserIdFetch = call(...)
value
val tweetByIdFetch = call(...)
ignore input and return a constant value.
val userByIdFetch = call(...)
call
send input to a service (with deduplication and batch- val user = value("jack").andThen(userByNameFetch)
ing), return its result. val userPat = [...] // { userId = uId }
map val userArrow = {
apply a function to input, return its result. val timeline = lookup("uId")
join .andThen(timelineByUserIdFetch)
send input to two arrows, return a tuple of their results. val timelineEntryPat = [...]
andThen // { tweetId = tId, authorId = aId }
send input to a1, send its result to a2. val timelineEntryArrow = {
val tweet = lookup("tId").andThen(tweetByIdFetch)
choose
val user = lookup("aId").andThen(userByIdFetch)
for each choice (pf, a) in choices, if pf is defined
pairWithArg(tweet).andThen(choose((
on input, send pf(input) to a, otherwise try the next tweetPat, // { text = text }
choice pairWithArg(user).andThen(choose((
sequence userPat, // { userName = userName }
send each element of input to a concurrently. join(lookup("userName"), lookup("text"))
and traverse, which applies an arrow uniformly to a se- )))
)))
quence, achieving our goal of reusing the static description
}
of a computation.
pairWithArg(timeline)
While the arrows interface in Stitch provides better per- .map { case (env, seq) => seq.map { v => (env, v) }
formance, it is much less convenient to use; in particular .andThen(sequence(
threading accumulated values through an arrow computa- choose((timelineEntryPat, timelineEntryArrow))
tions is tedious compared to using the lexical scope available ))
to a monadic interface. (One service at Twitter was converted }
to use arrows for performance, then converted back to the pairWithArg(user).andThen(
monadic interface when it was decided that readability was choose((userPat, userArrow))
worth the performance cost.) Fortunately compiling Stra- )
toQL to arrows provides both performance and convenience.
Figure 11. Compiled StratoQL
4.2 Compiling to Arrows
Following Hughes [8] (see Section 4.2), we compile a Stra- See Figure 11 for the compilation of Figure 1 into arrows.
toQL expression to an arrow that takes a value environment We use auxiliary functions lookup to look up bindings in the
as input and returns the value of the expression. environment and pairWithArg to carry the environment
A tuple expression compiles to a join of the compilations along with the result of an arrow.
of its elements, so the elements are evaluated concurrently; Atomic calls compile to uses of call arrows. As above,
similarly a for-expression compiles to a sequence, so the pattern bindings compile to choose calls. (Partial functions
body is evaluated concurrently for each element. which extract the bindings for a pattern are not shown.)
A pattern binding expression (match, val, or binding in a The sequence function applies its argument arrow to
for-expression) compiles to a choose over the compilations each element of the input sequence concurrently, and concur-
of its cases. The partial function for each case checks that rent atomic calls to the same underlying service are batched

10
Scala ’18, September 28, 2018, St. Louis, MO, USA Jacob Donham

together. So there are only 4 Thrift calls to underlying ser- complexity burden of a general-purpose language im-
vices: one each to userByName, timelineByUserId, plementation; and
tweetById, and userById. • we can implement domain-specific optimizations (for
(Note that the tweetById, and userById calls in the origi- example, avoiding deserializing data which is not ac-
nal query are independent and could be run concurrently, but tually accessed in the request path) which would be
our current compilation strategy for for-expressions doesn’t difficult in a general-purpose language implementa-
make this optimization.) tion.
We’re hopeful that as Strato and StratoQL mature, the Strato
5 Experience with Strato team will be less of a bottleneck for our users.
The Strato system has been under development for 3 years,
and taking production traffic for 2 years. Tens of teams use
Strato, and there are hundreds of microservices running on
6 Related Work
the platform. Strato powers a new API under development There are several service platforms available through public
for mobile clients based on GraphQL; eventually this will cloud providers, such as AWS Lambda, Google Cloud Func-
obviate the need for many existing HTTP services. And we tions, and Azure Functions. They typically support main-
are developing a system for processing event streams using stream languages such as Java or Javascript, and interface
hosted functions written in StratoQL. with other systems using arbitrary HTTP requests. They
It’s clear that teams using Strato enjoy increased lever- don’t attempt to provide a catalog of available services or
age: it saves them considerable effort, so enables them to describe them with a uniform type system.
deliver features more quickly, and experiment at lower risk; There are a large number of commercial “data virtual-
and because teams own and develop their own microservice ization” systems (e.g. Delphix, Denodo, and Gluent) that
configurations and can deploy them frequently, they retain make multiple data sources available in a single catalog and
a lot of autonomy. provide a uniform way to query them using SQL. They are
A platform like Strato supporting flexible application logic typically aimed at analytics use cases rather than high-scale
seems to demand some kind of programming language in application use cases, and don’t typically support embedded
which users can express logic; however the choice to develop application logic.
a new language is a complicated one. Haxl [10] shares many goals with Strato (particularly as it
On one hand, the requirement that teams use StratoQL is used at Facebook [9]); however Haxl is a domain-specific
costs them some autonomy: they must rely on the Strato language embedded in Haskell rather than an external DSL.
team to add missing features, fix bugs, write documentation, Compared to Stitch, Haxl has only a monadic interface, and
provide support, provide tooling (such as IDE integration), may be subject to the performance issues noted in Section 4.
etc., all of which would come “for free” with a stock language Lightweight Modular Staging [14] is an approach to em-
such as Scala. This has been a source of frustration for our bedding domain-specific code generation in a way that reuses
users. StratoQL’s support for implicit concurrency and native Scala syntax and typechecking. It does not address the issues
access to Thrift data are very convenient, but this is not the noted above that make it challenging to embed a general
main leverage teams get from Strato; it is rather avoiding purpose language implementation in a service platform.
the fixed costs of running a service. FrTime [5] is a domain-specific language that supports
On the other hand, a bespoke language and implementa- implicit functional reactivity by compiling to an underlying
tion makes it much easier to provide a service platform, for library, much as StratoQL supports implicit concurrency by
several reasons: compiling to Stitch. It implements an overhead-reducing
optimization similar to the one conjectured in Section 3.5.
• we don’t need to invoke an unwieldy, expensive com-
piler designed for a general-purpose language in order
to load configuration; Acknowledgments
• we have more control over runtime loading of code Strato has been the work of many people at Twitter: David
since we’re not tied to the JVM bytecode model; Benjamin, Marius Eriksen, Erik Froese, Ryan Greenberg,
• it’s easier to support computation over types that are Matthew Jeffryes, Alex Levenson, Runhang Li, Ran Magen,
defined in configuration and can be updated at run- Benjamin Navetta, Jonathan Simms, and Michael Solomon.
time; We would like to thank Joy Su, who managed the Strato
• it’s easier to limit the scope of what we must support team since its formation. We would also like to thank the
and operate since we can leave out or limit expensive many Strato customers who have contributed feedback, bug
language features; reports, and code to Strato.
• the implementation is small and simple, and we can Finally we would like to thank the anonymous reviewers
tailor it to our relatively narrow needs without the for their thorough and helpful comments.

11
A Domain-Specific Language for Microservices Scala ’18, September 28, 2018, St. Louis, MO, USA

References Data Access. In Proceedings of the 19th ACM SIGPLAN International


[1] Aditya Agarwal, Mark Slee, and Marc Kwiatkowski. 2007. Thrift: Conference on Functional Programming (ICFP ’14). ACM, New York, NY,
Scalable Cross-Language Services Implementation. Technical Report. USA, 325–337. https://doi.org/10.1145/2628136.2628144
Facebook. http://thrift.apache.org/static/files/thrift-20070401.pdf [11] Flemming Nielson and Hanne Riis Nielson. 1999. Type and Ef-
[2] Roberto M. Amadio and Luca Cardelli. 1991. Subtyping Recursive fect Systems. In Correct System Design, Recent Insight and Advances.
Types. In Proceedings of the 18th ACM SIGPLAN-SIGACT Symposium Springer-Verlag, Berlin, Heidelberg, 114–136. http://dl.acm.org/
on Principles of Programming Languages (POPL ’91). ACM, New York, citation.cfm?id=646005.673740
NY, USA, 104–118. https://doi.org/10.1145/99583.99600 [12] Martin Odersky, Christoph Zenger, and Matthias Zenger. 2001. Colored
[3] Apache Foundation. 2018. Aurora. https://github.com/apache/aurora/. Local Type Inference. In Proceedings of the 28th ACM SIGPLAN-SIGACT
(2018). [Online; accessed 21-May-2018]. Symposium on Principles of Programming Languages (POPL ’01). ACM,
[4] Apache Foundation. 2018. Mesos. https://github.com/apache/mesos. New York, NY, USA, 41–53. https://doi.org/10.1145/360204.360207
(2018). [Online; accessed 21-May-2018]. [13] Benjamin C. Pierce and David N. Turner. 2000. Local Type Inference.
[5] Kimberley Burchett, Gregory H. Cooper, and Shriram Krishnamurthi. ACM Trans. Program. Lang. Syst. 22, 1 (Jan. 2000), 1–44. https://doi.org/
2007. Lowering: A Static Optimization Technique for Transparent 10.1145/345099.345100
Functional Reactivity. In Proceedings of the 2007 ACM SIGPLAN Sympo- [14] Tiark Rompf and Martin Odersky. 2010. Lightweight Modular Staging:
sium on Partial Evaluation and Semantics-based Program Manipulation A Pragmatic Approach to Runtime Code Generation and Compiled
(PEPM ’07). ACM, New York, NY, USA, 71–80. https://doi.org/10.1145/ DSLs. SIGPLAN Not. 46, 2 (Oct. 2010), 127–136. https://doi.org/10.1145/
1244381.1244393 1942788.1868314
[6] Marius Eriksen. 2013. Your Server As a Function. In Proceedings of the [15] Peter Schuller. 2014. Manhattan, our real-time, multi-tenant
Seventh Workshop on Programming Languages and Operating Systems distributed database for Twitter scale. https://blog.twitter.com/
(PLOS ’13). ACM, New York, NY, USA, Article 5, 7 pages. https:// engineering/en%5Fus/a/2014/manhattan-our-real-time-multi-
doi.org/10.1145/2525528.2525538 tenant-distributed-database-for-twitter-scale.html. (2014). [Online;
[7] Facebook, Inc. 2016. GraphQL. http://facebook.github.io/graphql. accessed 21-May-2018].
(2016). [Online; accessed 21-May-2018]. [16] Twitter Engineering. 2011. Finagle: A Protocol-Agnostic RPC Sys-
[8] John Hughes. 2000. Generalising Monads to Arrows. Sci. Comput. tem. https://blog.twitter.com/engineering/en%5Fus/a/2011/finagle-
Program. 37, 1-3 (May 2000), 67–111. https://doi.org/10.1016/S0167- a-protocol-agnostic-rpc-system.html. (2011). [Online; accessed 21-
6423(99)00023-4 May-2018].
[9] Simon Marlow. 2013. The Haxl Project at Facebook. https:// [17] Twitter, Inc. 2018. Scrooge. https://twitter.github.io/scrooge/. (2018).
skillsmatter.com/skillscasts/4429-simon-marlow. (2013). [Online; [Online; accessed 21-May-2018].
accessed 21-May-2018].
[10] Simon Marlow, Louis Brandy, Jonathan Coens, and Jon Purdy. 2014.
There is No Fork: An Abstraction for Efficient, Concurrent, and Concise

12

You might also like