This file describes the architecture of the Nine Cards Backend.
In this section, we describe the environment of the application, and how it interacts with this environment.
The Nine Cards backend is a HTTP/REST application. Every operation is initiated by a call to an endpoint in the API: the backend has no other internal state changing operations. Most of the endpoints in the API are assumed to receive messages from a Nine Cards client. All the information sent from the backend to any client is sent in response to a client's HTTP request. The backend does not communicate directly with any Nine Cards client. For notifications, it uses the Firebase Cloud Messaging API.
The backend application interacts with several external systems and APIs outside of the application, for retrieving or storing information, or for sending notifications. The external systems are the following:
- Database. A PostgreSQL database is used to store information needed by the backend. This includes information about Nine Cards clients (users and devices), shared collections, the subsciptions of users to shared collections, and the information about countries.
- Google API. The Google API is used upon a client's signup, to check the identity of a new client.
- Google Analytics. A Google Analytics report collects aggregated statistics about the applications used in Nine Cards, such as which applications are more frequently added or removed to a moment, or which applications are most frequently used within each category. This data contains no information about the user except for the country, as given by Google Analytics. From this data, the backend builds the rankings of applications.
- Android Market. The API of the Android Market is used to retrieve information about the Android applications in the Play store, and also for searching lists of new applications either by name, by their category, or by their similarity to already installed apps.
- Play Store Web. Sometimes, the information about an app is not available in the Market API, so instead we retrieve it from each app's page in the Play Store website.
- Redis. We use a Redis in-memory store as a cache to keep data non-original data, that is to say, data that is retrieved from a remote source, or computed from a remote source. This includes the data about each application fetched from the Android Market API, and the most-valued application rankings computed from the Google Analytics Report.
- Firebase. The Firebase Cloud Messaging API is used to notify the subscribers of any shared collection of changes to the list of apps included in the shared collection.
The backend depends for its functionality on several external libraries, apart from the Scala
libraries.
The main libraries and frameworks used in the backend are the following ones:
-
Cats is a core library for the backend. The architecture follows the Data Types à la carte paper, and to implement such architecture we make use of
Cats
implementations forMonad
, for free monads, [natural transformations](cats.arrow.FunctionK
, and, in a few cases, monad transformers. -
Spray is used to build the
HTTP-REST
API, that serves as the external interface for the backend application. Thisapi
is used by each Nine Cards client to access the functionality of the backend. The entities transmitted through this API are all encoded in Json, for which we usespray-json
. -
Akka is used, but just enough to support the
spray
api. -
Circe, a library for implementing JSON encoding and decoding for data classes. Circe is used for a few classes in implementing the
api
, but it is mostly used for the communication with the external HTTP-REST services. -
Doobie is a purely-functional database access library for Scala, which we use for performing the queries and insertions into the database.
-
Http4s is an HTTP client that we use for implementing all the services that interact with the APIs of external services, such as Google Authentication, Analytics, Firebase, or the Android Market.
-
Enumeratum is used to represent
-
Testing of the different modules is done with a combination of Specs2 and ScalaCheck.
-
Shapeless is used as a utility in several places in the code, and it is relied upon by the libraries
Circe
,ScalaCheck
, orDoobie
.
The backend source code is organised into four modules, or sbt
projects, called
api
, processes
, services
, googleplay
, and commons
.
- The
commons
module contains the definition of the backend'sdomain
classes, and some utility method for common libraries, such ascats
orscalaz
. It also handles the environment configuration variables needed for each service interpreter. - The
api
module implements theREST-HTTP
application's Api, and the application's main method. It uses the librariesspray
,spray-json
(with a bit ofcirce
). - The
processes
module contains the application's process methods. Each process methods implements the functionality of one endpoint, using one or more services. The implementation follows a functional programming style, based on the use of monadic operations. - The
services
module implements the backend's services; each services retrieves and communicates data with an external system. The services layer is based on thedoobie
, andhttp4s
libraries. - The
googleplay
implements a subset of services, originally developed as a separate module, that interact with the Android Market API, and which use the Redis cache for internal storage.
The commons
module is available to all other modules. The dependencies among other modules are as follows:
the api
only depends on processes
; the processes
depends only on services
, and services
depends
on googleplay
.
This section describes the Functional-Programming design followed in writing the processes
and services of the backend. This design is inspired by the Data Types à la carte paper.
The key elements of this design are 1) a separation between a service's algebra of operations and
its interpreter, and 2) the use of the Free
monad for combining operations of different algebras.
Each service in the services
layer declares an algebra of operations, and an interpreter of those operations.
The algebra defines an abstract data type Ops[A]
, which represent the operations that can be done in that service.
Each operation can be seen as a command object,
which carries the input parameters needed to specify the value of the result.
As a guideline, each operation in an algebra should be atomic, and involve at most one interaction
with the external system in question.
The interpreter is the part of the service that takes the operations of the algebra and computes the results of that operation.
An Interpreter
is a (class of) object that implements a function from the service's algebra ADT
Ops[A]
to another parametric type, F[A]
, with the same base type A
.
Operations that transform from one parametric type F[A]
to another parametric type G[A]
are called natural transformations;
and in the backend every service interpreters must be a subtype of cats.arrow.FunctionK
.
The target type F
of the interpretation represents the kind of computation involved to obtain that result.
It can be Task
,
to represent an asynchronous computation of the result,
or ConnectionIO
,
to represent a computation that performs queries with doobie
,
or just Id
, to represent a result obtained just within the application (for testing).
Each process in the processes layer implements the functionality of one endpoint, and it usually involves
using different services.
For instance, the process to get the details of a shared collection involves reading the collection information
out of the database, and then retrieving form the Android Market API (or from the cache) the data of each
application in that collection. However, each service defines its operation in a separate algebra,
such as OpsDB[A]
or OpsRK[B]
.
To combine operations from several services, each operation of each algebra is injected into a Free
monad
Free[F,A]
. If the F
type parameter is the same, then the
Free
datatype provides
the operations for passing the results between the operations from different algebras.
This is achieved by using as that F
a Coproduct
of the different algebras operation types Ops[A]
.
The interpretation of the Coproduct
operations, then, becomes an alternative between them.