Jekyll2018-06-26T12:08:51+00:00/Galeria Kaufhof Technology BlogOur technology blogGALERIA Kaufhof GmbHmanuel.kiessling@kaufhof.deVideo-Mitschnitt: Vortrag zu ‘Modeling Domain Objects: Best Practices’2017-07-27T00:00:00+00:002017-07-27T00:00:00+00:00/general/2017/07/27/video-mitschnitt-vortrag-scala-user-group-cologne-modeling-domain-objects-best-practices<p>
Am 12. Juli 2017 waren wir erneut Gastgeber des <a href="https://www.meetup.com/de-DE/Scala-User-Group-Koln-Bonn/">monatlichen Scala Meetups in Köln</a>. Vortragender war der Kaufhof eShop Kollege <a href="https://github.com/valenterry">Valentin Willscher</a> zum Thema "Modeling Domain Objects: Best Practices".
</p>
<p>
<a href="http://valentin.willscher.de/presentations/tagged-types-introduction">Die Folien zum Vortrag</a> sind nun abrufbar, und es gibt auch einen
Videomitschnitt des Vortrags:
</p>
<center>
<iframe width="420" height="315" src="https://www.youtube.com/embed/xkVjuNGQ_NI" frameborder="0" allowfullscreen=""></iframe>
</center>
<p><br clear="all" /></p>manuelkiesslingAm 12. Juli 2017 waren wir erneut Gastgeber des monatlichen Scala Meetups in Köln. Vortragender war der Kaufhof eShop Kollege Valentin Willscher zum Thema "Modeling Domain Objects: Best Practices". Die Folien zum Vortrag sind nun abrufbar, und es gibt auch einen Videomitschnitt des Vortrags:Video-Mitschnitt: Vortrag zur eShop Architektur auf der OOP 20172017-03-09T00:00:00+00:002017-03-09T00:00:00+00:00/tutorials/2017/03/09/video-mitschnitt-vortrag-oop-2017-nutzen-und-herausforderungen-moderner-architektur-am-beispiel-des-galeria-kaufhof-online-shops<p>
Am 31. Januar 2017 hatten wir die Möglichkeit, den Architekturansatz des Galeria Kaufhof Online Shops auf der OOP 2017 im Rahmen eines Vortrags ausführlich zu erläutern:
</p>
<center>
<iframe width="420" height="315" src="https://www.youtube.com/embed/BBn9VsXhIyE" frameborder="0" allowfullscreen=""></iframe>
</center>
<p><br clear="all" /></p>manuelkiesslingAm 31. Januar 2017 hatten wir die Möglichkeit, den Architekturansatz des Galeria Kaufhof Online Shops auf der OOP 2017 im Rahmen eines Vortrags ausführlich zu erläutern:Scala Play2: Tolerant JSON body parsing with dedicated error handling2016-05-16T00:00:00+00:002016-05-16T00:00:00+00:00/tutorials/2016/05/16/scala-play2-tolerant-json-body-parsing-with-dedicated-error-handling<p>
I'm currently rewriting a Scala Play2 based web service that employs the following body parser:
</p>
<p>
<pre><code>def tolerantJsonParser[A](implicit reader: Reads[A]): BodyParser[A] =
parse.tolerantJson.validate(json =>
json.validate[A].asEither.left.map(err => Results.BadRequest)
)(play.api.libs.iteratee.Execution.Implicits.trampoline)
def doSomething = Action.async(tolerantJsonParser[SomeThing]) { request =>
val someThing: SomeThing = request.body
...</code></pre>
</p>
<p>
Not shown here is the implicit <code class="inline">Reads</code> that takes care of transforming the Json object into an object of case class <code class="inline">SomeThing</code> when the <code class="inline">json.validate</code> method is called.
</p>
<p>
As you can see, the existing code took care of handling the incoming Json in a tolerant manner - that is, it didn't bail out if the media type of the body is not <code class="inline">application/json</code>, which is what Play does per default, but which is not what we want in this case because clients send requests with a more specific media type to this webservice.
</p>
<p>
In case that the Json object to case class transformation fails, the body parser correctly answers the request with a <code class="inline">400 Bad Request</code> response status code. This covers all cases where the body is valid Json, but cannot be mapped to the structure of the case class using the implicit Reads.
</p>
<p>
Another case is implicitly covered, too - if the request body isn't even Json to begin with (e.g. because a <code class="inline">{</code> is missing, as in <code class="inline">"foo":"bar"}</code>), then <code class="inline">parse.tolerantJson</code> fails, resulting in a failure response, too.
</p>
<p>
However, the latter case is handled by Play2, resulting in a generic HTML error response - but for the rewrite, I wanted to have dedicated error handling because my goal was to send a specific Json encoded error response.
</p>
<p>
The solution turned out to be quite simple (which didn't stop me from taking several hours to come up with it) - by parsing the Json myself using the <code class="inline">parse.tolerantText</code> body parser, I gained full control over the body parsing process, which allowed me to react to errors in both steps in the process - the text-to-json transformation as well as the json-to-case-class-object transformation:
</p>
<p>
<pre><code>def tolerantTryJsonParser[A](implicit reader: Reads[A]): BodyParser[Try[A]] = {
parse.tolerantText.map { text =>
Try {
Json.parse(text).validate.get
}
}
}
def doSomething = Action.async(tolerantTryJsonParser[SomeThing]) { request =>
request.body match {
case Success(something) => ...
case Failure(error) => ...
}</code></pre>
</p>manuelkiesslingI'm currently rewriting a Scala Play2 based web service that employs the following body parser:How Cassandra’s inner workings relate to performance2016-02-29T00:00:00+00:002016-02-29T00:00:00+00:00/tutorials/2016/02/29/how-cassandras-inner-workings-relate-to-performance<h2 id="about">About</h2>
<p>At Galeria.de, we learned the hard way that it’s critical to understand the inner workings of the distributed
masterless database Cassandra if one wants to experience good performance during reads and writes. This post describes
some of the details of how Cassandra works under the hood, and shows how understanding these details helps to anticipate
which use patterns work well and which don’t.</p>
<h2 id="network-and-node-storage-architecture">Network and node storage architecture</h2>
<p>Roughly speaking, there are two main areas within the Cassandra architecture that play a deciding role with regards to
query performance - the network of nodes that form the database cluster, and the local storage on each of those nodes.</p>
<p>Efficient queries must be efficient network-wise as well as storage I/O wise. Let’s dig into both areas and see how
things work under the hood. If we understand the inner workings of both, we should be prepared to anticipate why certain
table-structure/query combinations are efficient and some are not.</p>
<h3 id="the-network">The network</h3>
<p>A production Cassandra setup always consists of multiple nodes, where a node is one Cassandra server process on one
system. All nodes are connected via the network. There isn’t any kind of “master” node - all nodes are created equal.</p>
<p>Logically, the data in a cluster is organized into keyspaces, which contain tables. Tables contain rows, and rows have
columns.</p>
<p>Physically, the content of a table row is always stored on the hard drive of at least one node in the cluster, and,
depending on how the keyspace has been defined upon creation, this row content is replicated to 0 or more other nodes
in the cluster. If all of this doesn’t make sense now, it will once you’ve read this post.</p>
<p>In this post, we always assume that our setup is a cluster of 5 nodes, numbered 1 to 5. A 5 node Cassandra cluster is
visualized as follows:</p>
<p><img width="50%" src="/assets/images/cassandra/Cassandra cluster.svg" /></p>
<p>Note that this is a logical visualization, not a network diagram - in reality each node talks to all other nodes in the
cluster, not only to its neighbours.</p>
<p>We further assume that we have created a keyspace “galeria” (which is going to hold the data tables we are going to
create) as follows:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CREATE KEYSPACE galeria WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor': 3 };
</code></pre></div></div>
<p>The replication factor defines that whatever row we insert into a table within this keyspace is stored on three
different nodes in the cluster.</p>
<p>We can now create a table “users” within this keyspace like this:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>USE galeria;
CREATE TABLE users (
username TEXT,
firstname TEXT,
lastname TEXT,
PRIMARY KEY (username)
);
</code></pre></div></div>
<p>When we insert a row into this table as follows:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>INSERT INTO users (username, firstname, lastname) VALUES ('jdoe', 'John', 'Doe');
</code></pre></div></div>
<p>then the following happens network-wise:</p>
<h4 id="step-1-client-coordinator-connection">Step 1: Client-Coordinator connection</h4>
<p>Our client (i.e., the process which issues the CQL statement) connects to a so-called <em>coordinator node</em>. This is not a
special node within our cluster - any node that happens to receive a query from a client also happens to act as the
coordinator for this query.</p>
<h4 id="step-2-mapping-the-partition-key-value-to-a-cluster-node">Step 2: Mapping the partition key value to a cluster node</h4>
<p>The first thing the coordinator node needs to do upon receiving the insert query is to find out where in the cluster the
row data for this INSERT needs to be persisted. Because <code class="highlighter-rouge">username</code> is the first (and in this case only) part of the
primary key, it acts as the partition key. The value of the partition key column is what’s used by the coordinator to
determine the first node onto which to store the row. To do so, a hash function - the so-called <em>partitioner</em> - is
applied on the value, and the result is a token. This token then tells the cluster about the target node, because each
node is responsible for a certain range of tokens. Assumed that tokens would run from 0 to 49 (in reality, the token
range is much larger), we can visualize the tokens-to-nodes mapping as follows:</p>
<p><img width="50%" src="/assets/images/cassandra/Cassandra cluster with tokens.svg" /></p>
<p>That is, node 3 holds those rows of table “users” in keyspace “galeria” for which the value of column “username” results
in a hash function token from 20 to 29.</p>
<p>For example, let’s just make up that the username column value “jdoe” would result in token value 17. This means that
the cluster must store the according row at least on node 2.</p>
<h4 id="step-3-determine-replication-nodes">Step 3: Determine replication nodes</h4>
<p>“At least” because what also comes into play is the replication factor of the keyspace holding the user table (which
contains the row in question). In our example, this factor is 3, which means that the row we create via the INSERT query
needs to be stored on two more nodes, besides its “main” node, 2. The algorithm for this is simple - additional
replicas of the row are stored on the next nodes clockwise in the ring - in our example, nodes 3 and 4.</p>
<p>Note that the replication of the write in question happens on all three nodes (2, 3, and 4) simultaneously, and not
one-after-the-other. This detail is important because it explains why Cassandra is relatively optimistic regarding the
to-disk-sync of a node’s commit log (see chapter “The node storage” for more on this).</p>
<p>As said, the replica order is based on the logical structure of the cluster - the cluster sees itself as an ordered ring
structure, where each node has a “neighbour” node that comes “after” it in the ring.</p>
<h4 id="step-4-wait-for-node-write-operations-to-finish-and-report-back-to-the-client">Step 4: Wait for node write operations to finish and report back to the client</h4>
<p>Once enough nodes have reported that their local write operations succeeded (see chapter “The node storage” for the
details), the coordinator node in turn reports the success of the INSERT operation back to the client. Here, “enough”
nodes depends on the <em>replication factor <-> query consistency level</em> relation for this operation. If we insert a row
into a table that belongs to a keyspace with a replication factor of <code class="highlighter-rouge">3</code>, and the query was issued with a consisteny
level of <code class="highlighter-rouge">QUORUM</code>, then <code class="highlighter-rouge">2</code> nodes (the quorum of 3 nodes) acknowledging the write is considered a success by the
coordinator.</p>
<h3 id="the-node-storage">The node storage</h3>
<p>Let’s “zoom in” on our node 2 and have a look at what happens in terms of its local storage when it receives the write
request from the coordinator node as a result of the INSERT query issued by the client. As noted, the same operations
happen on nodes 3 and 4 simultaneously.</p>
<p>While the final goal is to have the data stored in a so-called <em>SSTable</em> on disk, several moving parts are involved in
persisting the row data on a node:</p>
<p><img width="50%" src="/assets/images/cassandra/Cassandra node storage.svg" /></p>
<p>Why are there three different storage mechanisms combined in order to persist data and make data retrievable? The reason
is that only the interplay of these three mechanisms gives us a database that will, if used correctly, allow for
efficient data writes, efficient data reads, as well as durable storage of large amounts of data - which is great
because we certainly want a database that handles our INSERTs quickly, answers our SELECTs fast, doesn’t loose any of
our data while doing so, and stores more data than what fits into expensive and therefore limited memory.</p>
<p>So, looks like we have four important qualities that we want to have covered on the storage level: fast writes, fast
reads, plenty of capacity, durable storage.</p>
<p>Each one of the three mechanisms (commit log, MemTable, SSTables) covers at most three of these four qualities:</p>
<p>If we would only care about storing a lot of data in a durable way, the commit log would do - writing into it is fast,
and it is stored on the harddisk (but finding and reading all data for a desired row is very slow).</p>
<p>If we would only care about read and write performance, the MemTable would do - writing to and reading from it is fast
because all data of the MemTable lives in memory (but memory size is limited, and the data is not stored in a durable
way).</p>
<p>If we would only care about fast reads and durable storage of lots of data, then the SSTables would do, because these
are stored on disk in a structure that allows to quickly locate a desired data element (but writing data into this
structure is slow).</p>
<p>If we want to cover all four qualities, all three mechanisms need to be combined. Let’s see how this works in practice.</p>
<h4 id="the-commit-log">The commit log</h4>
<p>The first step taken storage-wise is to write the row data of our INSERT into the commit log.</p>
<p>The commit log is part of the process because it ensures data durability even in crash scenarios. Even if the server
crashes during a write operation - if the data made it into the commit log, our data is safe and it can be recovered
when the node is coming up again.</p>
<p>Note that, as mentioned above, Cassandra is quite optimistic in regards to actually syncing the commit log writes to
disk - per default, this happens every 10 seconds, but the node immediately acknowledges the write to the coordinator
(after also writing to the MemTable, see below), without waiting for the fsync. This means that there is a window of up
to 10 seconds during which, in case of a server crash, the data is not persisted on the harddrive of the crashing server
node, although the coordinator will think it is.</p>
<p>What in theory sounds highly problematic in terms of data durability isn’t a big deal in practice. Cassandra assumes
that data is always replicated, and two participating server nodes crashing within the same 10 seconds window is very
unlikely.</p>
<h4 id="the-memtable">The MemTable</h4>
<p>Next in line is the so-called MemTable. Why does the MemTable exist? When receiving a read request, Cassandra cannot
retrieve the requested data efficiently from the commit log - to allow for fast writes, it is append-only, which means
it contains data in the order that write requests arrived, which in turn means that in order to retrieve all data for a
row would mean to sequentially scan through the whole commit log from top to bottom, which would be prohibitively
expensive in terms of disk I/O.</p>
<p>The layout of SSTables, on the other hand, <em>is</em> optimized for efficient lookup of disk-stored row data. However,
Cassandra cannot update the SSTable holding the data for a given row synchronously upon each write, because this would
result in a huge amount of random disk I/O operations, making the write scenario prohibitively expensive in terms of
disk I/O. To circumvent this, SSTables are <em>never</em> updated - instead, they are created only from time to time, and are
only written to once, and are then immutable (read-only) - new, additional SSTables are created to cover new row data or
updates to existing row data.</p>
<p>Let’s close the circle: If row data cannot be retrieved from the commit log efficiently, and data isn’t put into
SSTables immediately, then another data structure is required in order to answer read requests <em>immediately</em> (as soon as
the data is written to the node) <strong>and</strong> <em>efficiently</em>.</p>
<p>And thus, MemTables come into play. Each server node has one MemTable for each table it carries. A MemTable lives,
as the name implies, in memory, and is mutable, i.e., row data is read from it and written to it as needed, which thanks
to the I/O performance of computer memory, is not prohibitively expensive - a fact that is nicely illustrated by one of
my all time favorite tables, where typical computer-world timings are compared to typical timings that humans can relate
to:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 CPU cycle 0.3 ns 1 s
Level 1 cache access 0.9 ns 3 s
Level 2 cache access 2.8 ns 9 s
Level 3 cache access 12.9 ns 43 s
Main memory access 120 ns 6 m
Solid-state disk I/O 50-150 μs 2-6 days
Rotational disk I/O 1-10 ms 1-12 months
Internet: SF to NYC 40 ms 4 years
Internet: SF to UK 81 ms 8 years
Internet: SF to Australia 183 ms 19 years
OS virtualization reboot 4 s 423 years
SCSI command time-out 30 s 3000 years
Hardware virtualization reboot 40 s 4000 years
Physical system reboot 5 m 32 millenia
</code></pre></div></div>
<p>Thus, reading and writing data from and to main memory versus from and to disk is like waiting for your salad order to
be finished in 6 minutes versus getting the salad somewhere between the day after tomorrow and next year. Sounds like a
MemTable does the job.</p>
<p>And thus, right after appending the INSERT data to the commit log, the node puts the same row data into the MemTable
structure. At this point, the data is both durable (commit log) and efficiently retrieveable (MemTable), and thus, the
data node can acknowledge the write to the coordinator node: Thanks, I have the data, and I’m able to provide it quickly
if anyone asks.</p>
<h4 id="the-sstables">The SSTables</h4>
<p>As already mentioned, this situation is fine for the moment, but without the third mechanism - SSTables - we’d quickly
run into problems once the node has to hold more data than the size of its memory allows.</p>
<p>SSTables certainly are the most interesting data structure of the three. A new SSTable is created whenever the MemTable
reaches a certain size (at which point it is considered “full”).</p>
<p>As said, SSTables are immutable, and this results in a certain fragmentation of row data.</p>
<p>Let’s assume we would issue the following three write statements, spread over a longer period of time:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>INSERT INTO users (username, firstname, lastname) VALUES ('jdoe', '', '');
UPDATE users SET firstname = 'John' WHERE username = 'jdoe';
UPDATE users SET lastname = 'Doe' WHERE username = 'jdoe';
UPDATE users SET firstname = 'John B.' WHERE username = 'jdoe';
</code></pre></div></div>
<p>Let’s further assume that between each of these operations, a lot of other CQL operations took place, and thus, between
these three operations, the MemTable of the target node became full several times and has been flushed into new
SSTables. Our row data is now distributed as follows:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Memtable: Knows nothing about the row anymore because it has been flushed
SSTable 1: Row data has been stored under partition key 'jdoe', with firstname = '' and lastname = ''
SSTable 2: Row data has been stored under partition key 'jdoe', with firstname = 'John'
SSTable 3: Row data has been stored under partition key 'jdoe', with lastname = 'Doe'
SSTable 4: Row data has been stored under partition key 'jdoe', with firstname = 'John B.'
</code></pre></div></div>
<p>Now imagine we would like to retrieve the full row data via
<code class="highlighter-rouge">SELECT firstname, lastname FROM users WHERE username = 'jdoe'</code>. It’s not enough to look into the newest SSTable,
because it only knows about the latest data change for the row. Cassandra has to go through all SSTables, and must put
together the full set of latest row data, while also resolving multiple updates to the same column using the timestamp
of the write event: In our case, the correct <em>firstname</em> value is <code class="highlighter-rouge">John B.</code> in SSTable 4, making the value stored in
SSTable 2 irrelevant.</p>
<p>As said, the structure of an SSTable is optimized for efficient reads from disk - entries in one SSTable are sorted by
partition key, and an index of all partition keys is loaded into memory when an SSTable is openend. Looking up
a row therefore is only one disk seek, with further sequential reads for retrieving the actual row data - thus, no
expensive random I/O needs to be performed.</p>
<p>However, if we run a Cassandra cluster over a long period of time, we get more and more SSTables. And because collecting
the actual data for a requested row means searching through more and more SSTables, row reads become less efficient over
time.</p>
<p>In order to avoid this kind of read performance deterioration, Cassandra runs a regular optimization process called
<em>compaction</em>. This process takes multiple SSTables files and combines them into one new SSTable file - the result could,
for example, look like this:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SSTable 1 (previously 1 & 2): Row data stored under partition key 'jdoe', with firstname = 'John' and lastname = ''
SSTable 2 (previously 3 & 4): Row stored under partition key 'jdoe', with firstname = 'John B.' and lastname = 'Doe'
</code></pre></div></div>
<p>After compaction, less files need to be searched in order to gather row data. (And there are other measures employed by
Cassandra in order to further reduce disk operations - for example, a bloom filter is used to determine SSTables that
can be skipped when looking up data).</p>
<h3 id="performance-of-cassandra-operations">Performance of Cassandra operations</h3>
<p>Musings about the performance of a Cassandra operation boil down to two questions: How much work will the coordinator
node have to do in terms of network I/O, and how much work will a participating data node have to do in terms of local
disk I/O?</p>
<p>The fastest operations are those for which the coordinator has to talk to only one data node, and where the approached
data node can find the requested information by searching as little as possible through as little SSTables as possible.
An expensive operation is one where the coordinator node has to talk to all nodes on the cluster, and where each of the
nodes has to scan a lot through many SSTables to retrieve the requested information.</p>
<p>However, if we <em>do</em> need to retrieve a lot of data, we actually <em>want</em> to shoulder the burden of bringing this data up
onto multiple nodes, in order to light the burden each node shoulder - that is, after all, one of the main reasons
for choosing a database that is distributed and therefore horizontally scalable.</p>
<p>That’s why so-called <em>hotspots</em> can be a problem. Imagine if our primary key is a column holding the day of week, as
follows:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CREATE TABLE logins (
day_of_week TEXT,
username TEXT,
PRIMARY KEY (day_of_week, username)
);
</code></pre></div></div>
<p>Every time a user logs into the system, we write their username into this table, with <code class="highlighter-rouge">day_of_week</code> set to <code class="highlighter-rouge">"monday"</code>,
<code class="highlighter-rouge">"tuesday"</code>, etc.</p>
<p>Because the partition key decides about the node that has to store data for a particular row, each and every write to
this table that happens on a Monday will have to be handled by the same node. Thus, the whole I/O burden for user login
logging will be on one particular node for the whole day (and on another node on another day). Even if your Cassandra
cluster is 1,000 nodes big - if the I/O throughput limit of the “Monday node” is exhausted on a given Monday, you will
run into problems when trying to log yet another login event for this day.</p>
<p>If we want to retrieve the list of all usernames that logged in on Monday, this is inefficient, too. The query</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SELECT username FROM logins WHERE day_of_week = 'monday';
</code></pre></div></div>
<p>will result in approaching the one node that maps to the <em>“monday”</em> partition key value, and the I/O burden of reading
through all SSTable entries for this day of week is on this one node, while all other nodes stay idle.</p>
<p>The need for balancing table structures and query strategies between the two, often conflicting, goals of spreading
data evenly around the cluster while also minimizing the number of partitions that have to be read is perfectly
explained in <a href="http://www.datastax.com/dev/blog/basic-rules-of-cassandra-data-modeling">Basic Rules of Cassandra Data Modeling</a>, the first and most important document to read if one wants to
use Cassandra.</p>
<p>While our post provides a glimpse under the hood, the Data Modeling post approaches its recommendations from an
“outside” perspective, and teaches the what and how of data modeling and querying. If you mix in the “inside”
view from our post, you should be well equipped to anticipate performance behaviours of your cluster. Look at a table
structure and the queries operating on it. Then think about what happens on the network, and what happens on the disk
of a node during query execution. This should set you on the right track for most cases.</p>
<p>As a final example for this, consider the following keyspace and table:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CREATE KEYSPACE galeria WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor': 1 };
CREATE TABLE products (
name TEXT,
color TEXT,
PRIMARY KEY (name, color)
);
</code></pre></div></div>
<p>We fill this as follows:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>INSERT INTO products (name, color) VALUES ('Towel', 'red');
INSERT INTO products (name, color) VALUES ('Towel', 'blue');
INSERT INTO products (name, color) VALUES ('Shirt', 'yellow');
INSERT INTO products (name, color) VALUES ('Shirt', 'red');
INSERT INTO products (name, color) VALUES ('Jacket', 'red');
INSERT INTO products (name, color) VALUES ('Jacket', 'blue');
</code></pre></div></div>
<p>Now let’s compare these two queries:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SELECT * FROM products WHERE name IN ('Towel', 'Jacket') AND color = 'red';
SELECT * FROM products LIMIT 1;
</code></pre></div></div>
<p>Which one is more efficient?</p>
<p>The first looks more complex - we are asking for two different primary keys, plus a clustering key value. The resultset
is two rows, the red towel and the red jacket.</p>
<p>The second one looks simple: All we need is one row.</p>
<p>But in fact, the second query is about 10x as complex as the first one when it comes to query execution. We can even
visualize this. Here is a screenshot showing the output of the statement trace for both queries (you get the tracing
output for all following queries by issueing <code class="highlighter-rouge">TRACING ON</code> on the CQL shell). The statement trace lists all network and
disk operations that need to be run in order to satisfy the query. I’ve taken screenshots of both text outputs, pasted
them next to each other, and rotated the image by 90 degree. The output for the first query is on top, the output for
the second is beneath it.</p>
<p><img width="50%" src="/assets/images/cassandra/tracing.gif" /></p>
<p>Although no detail information is visible at this scale, we can clearly see how the second query required way more
network and disk operations compared to the first. Why is this?</p>
<p>We have a keyspace with a replication factor of <code class="highlighter-rouge">1</code>, that is, the row data for one partition key is stored on one node
in the cluster. I’ve run both queries on a 3-node cluster. The first query specifically asks for two partition key
values - thanks to the hash algorithm, the coordinator node can calculate which nodes it needs to connect to, and on
those nodes, the index of the SSTables leads to the row data without unneccessary disk seek overhead.</p>
<p>The second query is much harder to satisfy: Because no partition key value is provided, the coordinator can not know
which nodes have rows for the table. It has to ask every single node. On each node, again because there is no partition
key value, each SSTable must be sequentially scanned for possible row data.</p>
<p>And this is only on a 3-node cluster. Imagine running the queries on a 1000-node cluster - not much changes for the
first query, because asking for two partition key values still means visiting at most two nodes (or only one, if both
values happen to resolve to the same node). But for the second query, the situation is even worse: now 1000 nodes need
to be visited for potential row data - even if only one single row is eventually returned.</p>
<p>With our understanding of the network and storage mechanisms employed by Cassandra, this kind of behaviour can be
anticipated, which helps to avoid unhealthy table structures and query strategies.</p>manuelkiesslingAboutCompile Time Cassandra Injection in Play 2.42016-01-17T00:00:00+00:002016-01-17T00:00:00+00:00/tutorials/2016/01/17/compile-time-cassandra-injection-in-play24<h2>About</h2>
<p>
<a href="https://www.playframework.com/documentation/2.4.x/Home">Play 2.4</a> supports <a href="https://www.playframework.com/documentation/2.4.x/ScalaCompileTimeDependencyInjection">Compile Time Dependency Injection</a>. This post describes how to inject your own Cassandra repository object into a controller at compile time, while also initializing and closing a Cassandra connection session during application startup and shutdown, respectively.
</p>
<p>
The code of the final application is available at <a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di">https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di</a>.
</p>
<h2>The goal</h2>
<p>
At the end of this post, we have created a small Play 2.4.6 Scala application with which will be able to serve the name of a <em>product</em> with a given <em>id</em> by reading information from a Cassandra database. We will be able to verify the correct behaviour of our application using a real database with an integration test and, because we will be able to mock the repository that is injected into the controller, we will also be able to verify the correct application behaviour without using a database.
</p>
<p>
The resulting code will be runnable and realistic, but also a bit simplistic - lacking error handling, for example - in order to be tractable.
</p>
<h2>Prerequisites</h2>
<p>
This post is aimed at readers who have already written Scala applications with Play2 and know how to work with sbt and Cassandra.
</p>
<p>
In order to compile and run the code, you <a href="https://www.playframework.com/documentation/2.4.x/Migration24#Java-8-support">need</a> a <a href="http://www.oracle.com/technetwork/java/javase/downloads/index.html">Java 8 SE Development Kit</a>, and you need a recent version of <a href="http://www.scala-sbt.org/download.html">sbt</a>.
</p>
<p>
Last but not least, you need to <a href="https://wiki.apache.org/cassandra/GettingStarted">set up a Cassandra cluster</a> - a one-node local setup is sufficient for the application that we'll create.
</p>
<p>
The post is written from the perspective of a Mac OS X system user with <a href="http://brew.sh/">Homebrew</a> installed, but should be adaptable for any Scala-capable environment with minor modifications.
</p>
<h2>Project setup</h2>
<p>
Let's start by creating an sbt-based Play 2.4 project using the <a href="https://www.typesafe.com/community/core-tools/activator-and-sbt">Typesafe Activator</a>, which we install via Homebrew: <code class="inline">brew install typesafe-activator</code>.
</p>
<p>
We can then use Activator to set up the Play2 project: <code class="inline">activator new play2-compiletime-cassandra-di play-scala</code>.
</p>
<p>
The first thing to do now is to switch from specs2 to ScalaTest as our testing framework, as described in <a href="http://manuel.kiessling.net/2015/12/31/play2-switching-from-specs2-to-scalatest">Play2: Switching from specs2 to ScalaTest</a>. Please change files <code class="inline">build.sbt</code>, <code class="inline">test/ApplicationSpec.scala</code>, and <code class="inline">test/IntegrationSpec.scala</code> as described there.
</p>
<p>
Running <code class="inline">sbt test</code> afterwards should just work. At this point, your codebase should look like <a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di/tree/3a96b61cf4445d80e3563e66d6e319c80eb8afc4">the reference repository at 3a96b61</a>.
</p>
<h2>Introducing the Cassandra driver</h2>
<p>We are now going to integrate the <a href="https://github.com/datastax/java-driver">Datastax Java Driver for Apache Cassandra</a>, roughly following the steps outlined in <a href="/tutorials/2015/01/14/setting-up-a-scala-sbt-multi-project-with-cassandra-connectivity-and-migrations.html">Setting up a Scala sbt multi-project with Cassandra connectivity and migrations</a> (but without tests and the migrations stuff to keep the codebase small for this post).</p>
<p>
This means adding the Cassandra driver as a dependency to file <code class="inline">build.sbt</code>, creating a utility class for connection URIs in file <code class="inline">app/cassandra/CassandraConnectionUri.scala</code>, and adding an object that handles database connections in file <code class="inline">app/cassandra/CassandraConnector.scala</code>. See <a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di/tree/0fa30e4874b1398f1557dbddd2346393d15c41e3">the resulting codebase on GitHub at 0fa30e4</a> or view <a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di/commit/0fa30e4874b1398f1557dbddd2346393d15c41e3?diff=split">the differences from the previous version of the codebase</a>.
</p>
<h2></h2>
<p>
With this, we can finally start to work on the actual Cassandra repository and learn how to inject it into a controller.
</p>
<p>
Again, I'm trying to keep things simple and the codecase lean. We will inject the repository into the existing <code class="inline">Application</code> controller in file <code class="inline">app/controllers/Application.scala</code>.
</p>
<p>
Our repository has only one job: It allows controllers to retrieve exactly one row from the Cassandra table it manages using a single-column primary key. Imagine we have a Cassandra table called <em>products</em>, with the following structure:
</p>
<p>
<pre><code>+----+-------+
| id | name |
+----+-------+
| 1 | Chair |
| 2 | Fork |
| 3 | Lamp |
+----+-------+</code></pre>
</p>
<p>
We can create the according table structure (and its keyspace) as follows:
</p>
<p>
<pre><code>CREATE KEYSPACE IF NOT EXISTS test WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };
USE test;
CREATE TABLE products (id INT PRIMARY KEY, name TEXT);</code></pre>
</p>
<p>
Let's make some assumptions: We expect our repository to provide a method <code class="inline">getOneById(id: Int): ProductModel</code>. Given a product id, the repository takes care of the heavy lifting that is required to return a <code class="inline">ProductModel</code> object which carries the data for this product as retrieved from the database.
</p>
<p>
We can start by declaring the model. Its a simple case class that lives in <code class="inline">app/models/ProductModel.scala</code>:
</p>
<p>
<pre><code>package models
case class ProductModel(id: Int, name: String)
</code></pre>
</p>
<p>
Next, we start building a repository structure which keeps the API towards clients of repositories abstract, and allows to create concrete implementations that work with a Cassandra database.
</p>
<p>
The first step is to define a trait that all repositories, be it concrete Cassandra-based implementations or lightweight mocks in test, will share. To do so, we put the following in file <code class="inline">app/repositories/Repository.scala</code>:
</p>
<p>
<pre><code>package repositories
abstract trait Repository[M, I] {
def getOneById(id: I): M
}
</code></pre>
</p>
<p>
Simple enough. This ensures that repository implementations will provide a <code class="inline">getOneById</code> method, and type parametrization allows to declare the type of parameter id and the type of the model object that has to be returned.
</p>
<p>
We know that we have to query the repository for integer values, and we know that we want to retrieve a <code class="inline">ProductModel</code> in return. Thus, we can already declare the fact that our Application controller depends on such a repository, in file <code class="inline">app/controllers/Application.scala</code>:
</p>
<p>
<pre><code>package controllers
import models.ProductModel
import play.api._
import play.api.mvc._
import repositories.Repository
class Application(productsRepository: Repository[ProductModel, Int]) extends Controller {
def index = Action {
Ok(views.html.index("Your new application is ready."))
}
}
</code></pre>
</p>
<p>
At this point (<a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di/tree/d58d681c07b9aec98a96a568b7d2a8f48b6954ab">commit d58d681 in the repo</a>, <a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di/commit/d58d681c07b9aec98a96a568b7d2a8f48b6954ab?diff=split">diff</a>), the application can no longer run and the existing test cases fail, because we have not yet implemented the mechanisms needed to actually inject the declared dependency into the controller. We will fix this later - first, we write a generic Cassandra repository implementation and use it to create a concrete implementation for the <code class="inline">Repository[ProductModel, Int]</code> type.
</p>
<p>
To do so, we create an abstract <code class="inline">CassandraRepository</code> class that does the heavy lifting, in file <code class="inline">app/repositories/CassandraRepository.scala</code>:
</p>
<p>
<pre><code>package repositories
import com.datastax.driver.core.querybuilder.QueryBuilder
import com.datastax.driver.core.querybuilder.QueryBuilder._
import com.datastax.driver.core.{Row, Session}
abstract class CassandraRepository[M, I](session: Session, tablename: String, partitionKeyName: String)
extends Repository[M, I] {
def rowToModel(row: Row): M
def getOneRowBySinglePartitionKeyId(partitionKeyValue: I): Row = {
val selectStmt =
select()
.from(tablename)
.where(QueryBuilder.eq(partitionKeyName, partitionKeyValue))
.limit(1)
val resultSet = session.execute(selectStmt)
val row = resultSet.one()
row
}
override def getOneById(id: I): M = {
val row = getOneRowBySinglePartitionKeyId(id)
rowToModel(row)
}
}
</code></pre>
</p>
<p>
Some notes on this class: in addition to the type parameters for the model and id field, a concrete class that extends this abstract CassandraRepository needs to provide a session representing the connection to a cassandra cluster, the name of the table that is to be wrapped, and the name of the field that is to be queried via getOneById. Furthermore, implementations must override the <code class="inline">rowToModel</code> method, because only a concrete implementation knows how to create a valid model from the values of a table row.
<br />
Finally, <code class="inline">getOneRowBySinglePartitionKeyId</code> is where stuff happens: using the session and the means provided by the DataStax driver, the database is queried and the resulting row is returned. Because CassandraRepository extends the Repository trait, it must override <code class="inline">getOneById</code> - in this case, that's simply a matter of retrieving the row using the code from the abstract class itself, and transforming it into a model using the to-be-overridden rowToModel method.
</p>
<p>
With this in place, the concrete implementation of a Cassandra-backed <code class="inline">ProductsRepository</code> class that matches the <code class="inline">Repository[ProductModel, Int]</code> type looks like this, in file <code class="inline">app/repositories/ProductsRepository.scala</code>:
</p>
<p>
<pre><code>package repositories
import com.datastax.driver.core.{Row, Session}
import models.ProductModel
class ProductsRepository(session: Session)
extends CassandraRepository[ProductModel, Int](session, "products", "id") {
override def rowToModel(row: Row): ProductModel = {
ProductModel(
row.getInt("id"),
row.getString("name")
)
}
}
</code></pre>
</p>
<p>
With these changes in place (<a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di/tree/cb7a9b48c470f277e86b21b80a7f7ca540335981">commit cb7a9b4 in the repo</a>, <a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di/commit/cb7a9b48c470f277e86b21b80a7f7ca540335981?diff=split">diff</a>), we can approach the injection. How can we control the way the <code class="inline">Application</code> controller is created, i.e. how can we control compile time dependency injection? In Play 2.4, this happens by providing a class that extends the <code class="inline">play.api.ApplicationLoader</code> trait.
</p>
<p>
To do so, create file <code class="inline">app/AppLoader.scala</code> with the following content:
</p>
<p>
<pre><code>import components.CassandraRepositoryComponents
import play.api.ApplicationLoader.Context
import play.api.routing.Router
import play.api.{Application, ApplicationLoader, BuiltInComponentsFromContext}
import router.Routes
class AppLoader extends ApplicationLoader {
override def load(context: ApplicationLoader.Context): Application =
new AppComponents(context).application
}
class AppComponents(context: Context) extends BuiltInComponentsFromContext(context) with CassandraRepositoryComponents {
lazy val applicationController = new controllers.Application(productsRepository)
lazy val assets = new controllers.Assets(httpErrorHandler)
override def router: Router = new Routes(
httpErrorHandler,
applicationController,
assets
)
}
</code></pre>
</p>
<p>
Play2 is not able to find out about this AppLoader itself, which is why we need to configure it in file <code class="inline">conf/application.conf</code> by adding the line <code class="inline">play.application.loader="AppLoader"</code>.
</p>
<p>
As you can see, we are overriding the part of Play2 that creates a runnable <code class="inline">play.api.Application</code> by instantiating the Application controller ourselves (while injecting the products repository, more on this later), and by overriding router creation. In order to get access to a products repository instance, the AppComponents class extends the <code class="inline">CassandraRepositoryComponents</code> trait. Within this trait, we connect to the database, set up the repository, and hook into the application lifecycle in order to shut down the database connection when the application shuts down. The code for all this goes into <code class="inline">app/components/CassandraRepositoryComponents.scala</code>:
</p>
<p>
<pre><code>package components
import cassandra.{CassandraConnector, CassandraConnectionUri}
import com.datastax.driver.core.Session
import models.ProductModel
import play.api.inject.ApplicationLifecycle
import play.api.{Configuration, Environment, Mode}
import repositories.{Repository, ProductsRepository}
import scala.concurrent.Future
trait CassandraRepositoryComponents {
// These will be filled by Play's built-in components; should be `def` to avoid initialization problems
def environment: Environment
def configuration: Configuration
def applicationLifecycle: ApplicationLifecycle
lazy private val cassandraSession: Session = {
val uriString = environment.mode match {
case Mode.Test => "cassandra://localhost:9042/test"
case _ => "cassandra://localhost:9042/prod"
}
val session: Session = CassandraConnector.createSessionAndInitKeyspace(
CassandraConnectionUri(uriString)
)
// Shutdown the client when the app is stopped or reloaded
applicationLifecycle.addStopHook(() => Future.successful(session.close()))
session
}
lazy val productsRepository: Repository[ProductModel, Int] = {
new ProductsRepository(cassandraSession)
}
}
</code></pre>
</p>
<p>
As you can see, this approach also allows to adapt to the application environment: In our case, we connect to a different Cassandra cluster URI if we are running in the test environment.
</p>
<p>
At this point (<a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di/tree/4e707cb784e7adbc7c8204f033404690986c9448">commit 4e707cb</a>, <a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di/commit/4e707cb784e7adbc7c8204f033404690986c9448?diff=split">diff</a>, dependency injection is in place and the application is runnable again.
</p>
<p>
What still doesn't run, however, are the tests. Also, it's a bit sad that we did all those things that give our controller a repository, and then it doesn't even use it. Let's fix both issues.
</p>
<p>
We start with the integration spec in file <code class="inline">test/IntegrationSpec.scala</code>. We are going to extend this quite a bit:
</p>
<p>
<pre><code>import java.io.File
import cassandra.{CassandraConnector, CassandraConnectionUri}
import org.scalatest.BeforeAndAfter
import org.scalatestplus.play._
import play.api
import play.api.{Mode, Environment, ApplicationLoader}
class IntegrationSpec extends PlaySpec with OneBrowserPerSuite with OneServerPerSuite with HtmlUnitFactory with BeforeAndAfter {
before {
val uri = CassandraConnectionUri("cassandra://localhost:9042/test")
val session = CassandraConnector.createSessionAndInitKeyspace(uri)
val query = "INSERT INTO products (id, name) VALUES (1, 'Chair');"
session.execute(query)
session.close()
}
override implicit lazy val app: api.Application =
new AppLoader().load(
ApplicationLoader.createContext(
new Environment(
new File("."), ApplicationLoader.getClass.getClassLoader, Mode.Test)
)
)
"Application" should {
"work from within a browser and tell us about the first product" in {
go to "http://localhost:" + port
pageSource must include ("Your new application is ready. The name of product #1 is Chair.")
}
}
}
</code></pre>
</p>
<p>
We need to override the implicit <code class="inline">app</code> value, where we ask our new AppLoader to create an application for the <em>Test</em> environment. Furthermore, we extend our specification and now expect our app to not only greet us, but to also tell us about the name of the product with ID 1, which we insert in the new <em>before</em> step of our specification.
</p>
<p>
We could do the same with the ApplicationSpec, but let's go one step further and mock the ProductRepository, which gives us a specification that in contrast to the integration spec doesn't need an actual Cassandra database to work, and only verifies the behaviour of the Application controller itself, not its dependencies. To do so, we change file <code class="inline">test/ApplicationSpec.scala</code> as follows:
</p>
<p>
<pre><code>import java.io.File
import models.ProductModel
import play.api
import play.api.{Mode, Environment, ApplicationLoader}
import play.api.ApplicationLoader.Context
import play.api.test._
import play.api.test.Helpers._
import org.scalatestplus.play._
import repositories.Repository
class MockProductsRepository extends Repository[ProductModel, Int] {
override def getOneById(id: Int): ProductModel = {
ProductModel(1, "Mocked Chair")
}
}
class FakeApplicationComponents(context: Context) extends AppComponents(context) {
override lazy val productsRepository = new MockProductsRepository()
}
class FakeAppLoader extends ApplicationLoader {
override def load(context: Context): api.Application =
new FakeApplicationComponents(context).application
}
class ApplicationSpec extends PlaySpec with OneAppPerSuite {
override implicit lazy val app: api.Application = {
val appLoader = new FakeAppLoader
appLoader.load(
ApplicationLoader.createContext(
new Environment(
new File("."), ApplicationLoader.getClass.getClassLoader, Mode.Test)
)
)
}
"Application" should {
"send 404 on a bad request" in {
val Some(wrongRoute) = route(FakeRequest(GET, "/boum"))
status(wrongRoute) mustBe NOT_FOUND
}
"render the index page and tell us about the first product" in {
val Some(home) = route(FakeRequest(GET, "/"))
status(home) mustBe OK
contentType(home) mustBe Some("text/html")
contentAsString(home) must include ("Your new application is ready. The name of product #1 is Mocked Chair.")
}
}
}
</code></pre>
</pre>
</p>
<p>
Of course, both specs will only pass if we change the behaviour of the Application controller in file <code class="inline">app/controllers/Application.scala</code> and make us of the injected repository:
</p>
<p>
<pre><code>package controllers
import models.ProductModel
import play.api._
import play.api.mvc._
import repositories.Repository
class Application(productsRepository: Repository[ProductModel, Int]) extends Controller {
def index = Action {
val product = productsRepository.getOneById(1)
Ok(views.html.index(s"Your new application is ready. The name of product #1 is ${product.name}."))
}
}
</code></pre>
</p>
<p>
And that's it. At <a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di/tree/796b6c64ea88a160481069261e1474ec36103072">commit 796b6c6</a>, (see <a href="https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di/commit/796b6c64ea88a160481069261e1474ec36103072?diff=split">the diff</a>), we have a working Play 2.4 app with a compile time injected Cassandra repository.
</p>manuelkiesslingAbout Play 2.4 supports Compile Time Dependency Injection. This post describes how to inject your own Cassandra repository object into a controller at compile time, while also initializing and closing a Cassandra connection session during application startup and shutdown, respectively. The code of the final application is available at https://github.com/Galeria-Kaufhof/play2-compiletime-cassandra-di.Video: Die Arbeitswelt bei Galeria.de2016-01-15T00:00:00+00:002016-01-15T00:00:00+00:00/general/2016/01/15/video-die-arbeitswelt-bei-galeria-de<p>Gemeinsam mit dem GALERIA Kaufhof Personalmarketing sind die folgenden vier Videos entstanden, die die Arbeitswelt in IT
und Produktmanagement bei Galeria.de präsentieren und Einblick geben in unsere Organisation und Kultur.</p>
<p>Das Hauptvideo gibt einen Überblick, und in den weiteren Videos geht es im Detail um die Arbeit als Interface Developer,
Product Owner, und Leiter der technischen Plattform.</p>
<style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style>
<div class="embed-container"><iframe src="https://www.youtube.com/embed/3ZpU_fjTqQc" frameborder="0" allowfullscreen=""></iframe></div>
<div class="left" style="width: 33.3333333%; float: left;">
<style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class="embed-container"><iframe src="https://www.youtube.com/embed/f3hGtayDOUU" frameborder="0" allowfullscreen=""></iframe></div>
</div>
<div class="middle" style="width: 33.3333333%; float: left;">
<style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class="embed-container"><iframe src="https://www.youtube.com/embed/98DPSrAVaxA" frameborder="0" allowfullscreen=""></iframe></div>
</div>
<div class="right" style="width: 33.3333333%; float: left;">
<style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class="embed-container"><iframe src="https://www.youtube.com/embed/DE9uafs6IHk" frameborder="0" allowfullscreen=""></iframe></div>
</div>
<div> </div>manuelkiesslingGemeinsam mit dem GALERIA Kaufhof Personalmarketing sind die folgenden vier Videos entstanden, die die Arbeitswelt in IT und Produktmanagement bei Galeria.de präsentieren und Einblick geben in unsere Organisation und Kultur.Die Architektur der Galeria.de Plattform im Kontext der Produktentwicklungsorganisation2015-12-15T00:00:00+00:002015-12-15T00:00:00+00:00/general/2015/12/15/architektur-und-organisation-im-galeria-de-produktmanagement<h2 id="über-diesen-artikel">Über diesen Artikel</h2>
<p>Im Kontext dieses Blogs stellt der vorliegende Artikel ein Update und eine Erweiterung des <a href="/general/2014/09/20/jump-ein-technologiesprung-bei-galeria-kaufhof.html">Beitrags von September
2014</a> dar.</p>
<p>Schon damals existierte ein klar definiertes Set an Vorgaben, welches den Rahmen für Makro- und Mikroarchitekturfragen
gesteckt hat und das Projekt in Hinblick auf Fragen der System- und Softwarearchitektur leitet.</p>
<p>In den vergangenen Tagen haben wir begonnen, ausgehend von den Erfahrungen bis heute und unserer jetzigen Perspektive,
einige der Grundlagen unserer Architektur noch einmal neu aufzuschreiben.</p>
<p>Anstoß hierzu lieferte unter anderem der Launch von <a href="http://scs-architecture.org/">http://scs-architecture.org/</a>, einem Portal,
welches das Konzept der Self-contained Systems, die den zentralen Baustein auch unserer Architektur bilden, präsentiert.</p>
<p>Inhaltlich haben wir das Konzept SCS seit langem gelebt, aber semantisch war der Ansatz auf unserer Architekturlandkarte
nicht klar verortet. Mit der Überarbeitung haben nun alle zentralen Bausteine einen klaren Platz und einen klaren Namen.</p>
<p>Bei einem Projekt, welches seit nunmehr fast 2 Jahren läuft und seit fast einem Jahr im Betriebs- und
Weiterentwicklungsmodus ist, wird auch die Frage des fachlichen Wachstums spannend. So klar die bestehende Struktur ist</p>
<ul>
<li>was sind die architektonischen Leitlinien, wenn der fachliche Themenumfang wächst und die Plattform sich
inhaltich weiterentwickelt? Schliesslich stehen wir noch am Anfang unserer Mission, MCR-Marktführer in Europa zu werden.</li>
</ul>
<p>Zusätzlich klingt in diesem Dokument auch das Verhältnis zwischen Produktarchitektur und
Produktentwicklungsorganisation stärker an (ohne dabei den Anspruch zu erheben, die Aufbau- und Ablauforganisation der
Galeria.de Produktentwicklung vollumfänglich aufzuzeigen - dies muss im Zuge anderer Beiträge erfolgen).</p>
<h2 id="die-visualisierung">Die Visualisierung</h2>
<p>Einen Überblick über die verschiedenen Architekturkomponenten und ihr Verhältnis zueinander soll das folgende Schaubild
ermöglichen:</p>
<p><img width="100%" src="/assets/images/architektur-update/Ueberblick_Architektur_GALERIA_Kaufhof_Online_Plattform.svg" /></p>
<h2 id="grundlagen-der-architektur">Grundlagen der Architektur</h2>
<p>Zwei Grundideen bilden das Fundament der architektonischen Strukturierung: Eine vertikale Orientierung der High-Level
Komponenten in sogenannten Self-contained Systems, und eine fachlich motivierte Trennung und Gruppierung dieser
Komponenten in sogenannten Domänen.</p>
<p>Das Verhältnis von Domänen zu Systemen ist wie folgt: eine Domäne liegt immer dann vor, wenn ein oder
mehrere Systeme einen logisch zusammenhängenden Ausschnitt der fachlichen Use-Cases eines Benutzers vollumfänglich
abbilden. Konkretes Beispiel: die Domäne SEARCH bei Galeria.de umfasst diejenigen Systeme, welche von der
Benutzeroberfläche bis zur Datenhaltung das Suchen und Finden von Produkten für den Benutzer von Galeria.de ermöglichen.</p>
<p>Der Domäne SEARCH ist also mindestens ein System zugeordnet, welches sowohl die Weboberflächen-Elemente (wie zum
Beispiel die Suchbox mit Auto-Complete, Suchergebnisseite usw.) bereitstellt, als auch den Import von Produktdaten und
deren Überführung in eine spezialisierte Such-Datenbank implementiert.</p>
<p>Ein System wiederum ist eine Sammlung von Anwendungen mit gemeinsamer Datenhaltung, welche in unserem Fall auch alle
innerhalb desselben Code Repositories liegen und auch gemeinsam deployed werden - bei einer Scala Domäne könnte es sich
also konkret um ein <a href="http://www.scala-sbt.org/0.13/tutorial/Multi-Project.html">sbt Multiprojekt</a> bestehend aus einer
Play2 Anwendung für das Webinterface und zusätzlich Anwendungen auf Basis von Akka für die Hintergrundverarbeitung von
Daten handeln. Und unsere Ruby Domäne CONTROL wiederum betreibt ein System, welches sich intern stark in Richtung einer
Microservice-basierten Struktur entwickelt hat und als gemeinsame Datenhaltung unter anderem einen
MessageBus-orientierten Ansatz verfolgt.</p>
<p>Miteinander sprechen diese Systeme - innerhalb einer Domänengrenze und darüber hinaus - nur über definierte
Schnittstellen (da sie keine Daten teilen dürfen), und dies unter Vermeidung von verteilten Callstacks.</p>
<p>In gewissem Sinne wird hier das bekannte Paradigma von loser Kopplung und hoher Kohäsion, welches klassischerweise auf
Ebene eines einzelnen Softwaresystems betrachtet wird, auf einer höheren Ebene fortgesetzt.</p>
<p>Die Kohäsion entsteht, weil fachlich verwandte Themen vereint werden in den Self-contained Systems einer Domäne. Die
lose Kopplung wird abgebildet dadurch, dass die verschiedenen Systeme nur über Schnittstellen miteinander kommunizieren.</p>
<p>Damit gilt für das Gesamtsystem dieselbe Eigenschaft, die auch innerhalb eines Softwaresystems gilt, welches nach diesem
Paradigma entworfen wurde: Änderungen in einer Komponente bedingen nur dann Änderungen in einer anderen Komponente,
wenn die Änderungen die Schnittstelle betreffen.</p>
<p>Dies sorgt für hohe Robustheit des Gesamtsystems (ohne verteilte Callstacks und dank Replikation von Daten können andere
Systeme weiter operieren, auch wenn ein angebundenes System nicht-verfügbar wird), ermöglicht weitgehend autarkes
Arbeiten pro Domäne (nicht zuletzt in Hinblick auf die Releasefrequenz), bietet die Möglichkeit, Systeme nach ihren
unterschiedlichen Anforderungen auch unterschiedlich zu skalieren, und erlaubt eine in Hinblick auf die erforderliche
Funktionalität passgenaue Wahl der Technologien pro System. Weitere Informationen hierzu liefert
<a href="http://scs-architecture.org">scs-architecture.org</a>.</p>
<p>Das Konzept der Domäne ist weiterhin der Brückenschlag zwischen Architektur und Aufbauorganisation. Optimalerweise steht
hinter jeder Domäne ein Team - in unserem Fall ein Scrum-Team - welches von Anforderungsmanagent über
Software-Entwicklung und QA bis hin zum Betrieb die Domäne mit ihren Systemen fachlich und technisch vollumfänglich
“owned”.</p>
<h2 id="domänen-und-ihre-systeme">Domänen und ihre Systeme</h2>
<p>Warum dann noch die Unterscheidung zwischen Domäne und System? Warum nicht 1 Domäne gleich 1 System? An dieser Stelle
findet derzeit eine Evolution unseres bisherigen Modells statt, in dem die Begriffe bisher oft synonym verwendet
wurden.</p>
<p>Die Grund für eine Unterscheidung ist, dass ein Komponentenschnitt einerseits fachlich motiviert sein kann, andererseits
technisch. Eine rein technische Motivation führt hierbei zu einem neuen System, eine fachliche Motivation zu einer neuen
Domäne. Der Begriff der “technischen Motivation” ist allerdings recht weit gefasst - es muss nicht zwangsläufig die
Einführung einer neuen Technologie (Programmiersprache, Framework, Datenbanksystem usw.) vorliegen: selbst bei
gleichbleibendem Technologiestack kann es die technische Motivation geben, eine weiterhin saubere Codebase gewährleisten
zu wollen oder feingranularer releasen zu wollen.</p>
<p>Für beide Motivationen gibt es derzeit Beispiele im Projekt. Das Team der bestehenden Domäne EXPLORE, welches sich
bisher vornehmlich um Teaser und Störer im Shop kümmert, soll in Zukunft die Verantwortung übernehmen für die
Infrastruktur von Inhalten auf Galeria.de, die nicht direkt mit dem Shopping-Erlebnis des Kunden zu tun haben,
beispielsweise Presseseiten und Unternehmensinformationen sowie Inhalte rund um das Recruiting.</p>
<p>Abgesehen davon, dass hier auf Basis eines Open Source CMS auch technisch eine neue Lösung entsteht, wird schnell klar,
dass die bisherige EXPLORE Fachlichkeit verlassen wird. Daher begründet das Team derzeit eine neue Domäne CONTENT. Auch
wenn hier vorerst dieselben handelnden Personen an Bord sind, sehen wir das zu behandelnde Thema aufgrund der
andersartigen fachlichen Ausrichtung als neue fachliche Einheit.</p>
<p>Anders im Team ORDER, welches sich um alle Fachlichkeiten rund um das Thema Bestellungen kümmert. Hier zeichnet sich ab,
dass es sinnvoll sein könnte, die Webshop-orientierten Aspekte des Bestellens von der nachgelagerten Bestellverarbeitung
zu trennen - sinnvoll zum Beispiel vor dem Hintergrund der möglicherweise sehr unterschiedlichen
Skalierungsanforderungen für den Shop einerseits und die nachgelagerte Verarbeitung von Bestellungen andererseits;
weiterhin ist zu erwarten, dass eine Entkopplung dieser beiden Aspekte auch in Hinblick auf Releasezyklen und Sauberkeit
der lokalen Architektur Vorteile bringt. Self-contained Systems sind stets Monolithen - das ist nicht per se negativ,
aber jeder Monolith kann irgendwann zu groß werden; “zu groß” ist eine sehr subjektive Eigenschaft, aber ein Maßstab ist
sicherlich die mittlerweile weitverbreitete Formulierung “passt nicht mehr vollständig in den Kopf eines Teammitglieds”.</p>
<h2 id="systeme-und-umgebungen">Systeme und Umgebungen</h2>
<p>Das Schaubild spricht weiterhin von Umgebungen. Gemeint ist damit der Kontext, in dem die Anwendungen von Systemen
laufen können. Aufgrund der vertikalen Orientierung von Systemen ist der Ausführungskontext ihrer Anwendungen potentiell
verteilt: Eine Play2-basierte Scala Anwendung wird auf den Servern von Galeria.de ausgeführt, also in der sogenannten
Plattformumgebung - diese Backend-Anwendung liefert aber vielleicht eine Single-Page Application oder eine andere Form
von JavaScript-Anwendung aus; diese läuft dann im Browser des Benutzers, also in dessen Umgebung.</p>
<p>Weiterhin sprechen viele Systeme der Plattformumgebung mit Fremdsystemen, im Falle von Galeria.de beispielsweise mit
Systemen der Warenwirtschaft. Diese werden ausgeführt in einer Fremdumgebung, also einer Umgebung die technisch und
organisatorisch außerhalb der Kontrolle von Galeria.de liegt.</p>
<h2 id="schnittstellen">Schnittstellen</h2>
<p>Der Verzicht auf eine gemeinsame Datenhaltung bedingt klar definierte und zuverlässig arbeitende Schnittstellen.
Leitbild ist für uns das World Wide Web, weshalb wir auf HTTP als Transportprotokoll setzen und unsere Schnittstellen
nach den Prinzipien von REST gestalten.</p>
<p>Wir unterscheiden hierbei 4 Typen von Schnittstellen:</p>
<ul>
<li>Typ “Web”
<ul>
<li>baut auf HTTP auf</li>
<li>RESTful</li>
<li>HATEOASful</li>
<li>benutzerorientierter Media-Type</li>
<li>klassischerweise die von einer Backend-Anwendung generierte, HTML-basierte Webseite (oder Webseiten-Elemente, welche
durch die Frontend-Integration per SSI eingebunden werden)</li>
</ul>
</li>
<li>Typ “REST”
<ul>
<li>baut auf HTTP auf</li>
<li>RESTful</li>
<li>HATEOASful</li>
<li>maschinenorientierter Media-Type</li>
<li>klassischerweise der durch eine Backend-Anwendung bereitgestellte JSON-Webservice, welcher von JavaScript
Anwendungen, Mobile Apps, oder Fremdanwendungen genutzt wird</li>
</ul>
</li>
<li>Typ “Multipart”
<ul>
<li>baut auf HTTP auf</li>
<li>RESTful</li>
<li>maschinenorientierter Media-Type</li>
<li>Auf HTTP multipart basierende Feed- oder Snapshot-Schnittstelle für die Synchronisation von Massendaten und für
Event-Sourcing</li>
</ul>
</li>
<li>Typ “Other”
<ul>
<li>Schnittstellen, die nicht den anderen Typen entsprechen: FTP, SOAP, usw. Unterschiedlichste Transportprotokolle und
Medientypen sind denkbar.</li>
</ul>
</li>
</ul>
<h2 id="zusammenfassung">Zusammenfassung</h2>
<p>Der Architekturansatz von vertikal orientierten Self-contained Systems gepaart mit fachlich geschnittenen Domänen und
HTTP-basierten Schnittstellen bildet den Rahmen für alle Produktentwicklungsinitiativen von Galeria.de.</p>
<p>Dieser Rahmen ermöglicht optimale Kundenorientierung durch fachliche Spezialisierung in den Domänenteams, ein robustes
Gesamtprodukt dank der Entkopplung von Datenhaltung und Callstacks, und eine effektive Weiterentwicklung dank autarker
Releaseprozesse und einem auf die Schnittstellen beschränkten Abstimmungsprozess in den Softwareentwicklungsteams.</p>manuelkiesslingÜber diesen ArtikelEntwicklung und Betrieb einer Symfony2 Webanwendung - Teil 12015-10-27T00:00:00+00:002015-10-27T00:00:00+00:00/tutorials/2015/10/27/entwicklung-und-betrieb-einer-symfony2-webanwendung-teil-1<h2 id="über-diesen-artikel">Über diesen Artikel</h2>
<p>Vor kurzem standen wir vor der Herausforderung, eine kleine Onlineanwendung für eine zeitlich begrenzte Rabattaktion zu realisieren, die keinerlei Verbindung mit dem Galeria.de Webshop hatte.</p>
<p>Während <a href="/general/2014/09/20/jump-ein-technologiesprung-bei-galeria-kaufhof.html">der Technologiestack rund um unseren Onlineshop</a> auf Scala, Ruby und Casssandra basiert, wurde hier die Entscheidung gefällt, die Anwendung außerhalb unserer bestehenden Dienste und Systeme zu realisieren, und auch nicht im Kontext unserer Scala und Ruby Teams, mit dem Ziel den normalen Produktentwicklungsprozess nicht mit diesem Sonderprojekt zu “stören”.</p>
<p>Weiterhin waren hier die für den Online-Shop geltenden Skalierungsanforderungen, die ein zentraler Treiber hinter den technologischen Entscheidungen unseres Hauptstacks sind, nicht von Belang.</p>
<p>Da die Deadline für dieses Projekt sehr knapp gesteckt war, hat man sich der technologischen Lösung sehr pragmatisch genähert. Es fiel die Entscheidung, die Anwendung mit dem PHP-basierten Symfony2 Framework und MySQL als Datenbank zu bauen. Diese Kombination ist sehr gut etabliert und hat sich als genau die richtige Wahl für diese Art von Projekt herausgestellt.</p>
<p>Ich möchte dieses Projekt aus der realen Welt heranziehen um den Leser durch all jene Details des Produktentwicklungsprozesses zu führen, die eine relevante Rolle spielen im Zusammenhang mit dem Schreiben und Betreiben von Anwendungen auf Basis von Symfony2 - hierbei gehe ich ein auf Aspekte wie Projektsetup, Testing, Datenbankmigrationen, Continuous Delivery, Sicherheit, und vieles mehr.</p>
<p>Der hierzu gewählte Ansatz hebt alle signifikanten Entscheidungen hervor, erklärt die Implementationsdetails die sich aus diesen Entscheidungen ergaben, und diskutiert die Vor- und Nachteile dieser Entscheidungen. Ich werde weiterhin diejenigen Teile der Anwendungen herausstellen, die weiter verbessert werden könnten.</p>
<h2 id="zielgruppe">Zielgruppe</h2>
<p>Dieser Beitrag richtet sich an PHP-Entwickler, die mindestens erste Erfahrungen in der Arbeit mit Symfony2 haben, und für die bspw. die Arbeit mit Composer bekanntes Terrain ist.</p>
<h2 id="die-anforderungen">Die Anforderungen</h2>
<p>Mit Wirkung zum 1. Oktober 2015 wurde die GALERIA Kaufhof GmbH Teil der Hudson’s Bay Company. Zuvor waren wir Teil der METRO GROUP. Vor Abschluss dieser Transaktion wurde eine allerletzte Rabattaktion für unsere nunmehr ehemaligen Kolleginnen und Kollegen der METRO aufgesetzt: <em>Good Buy METRO</em>.</p>
<p>Der Use Case sah wie folgt aus: Für eine begrenzte Zeit konnten sich Mitarbeiterinnen und Mitarbeiter bestimmter METRO-Tochterunternehmen über die hier beschriebene Webanwendung für die Rabattaktion registrieren, basierend auf ihrer Mailadresse und Personalnummer. Nach Abschluss der Registrierung erhielt jeder Benutzer eine Mail mit einem PDF-Anhang, auf dem insgesamt sechs personalisierte Gutscheine abgedruckt waren. Jeder Gutschein enthielt einen QR Code, der an der Kasse einer unserer Filialen eingescannt werden konnte, um den Rabatt auf den Einkauf zu erhalten.</p>
<p>Im Kern lauteten die funktionalen Anforderungen daher:</p>
<ul>
<li>Erlaube Zugriff auf eine Webanwendung</li>
<li>Ermögliche über die Webanwendung eine Registrierung auf Basis von Mailadresse und Personalnummer</li>
<li>Verifiziere die Gültigkeit der Personalnummer über einen internen Prozess, sowie die Gültigkeit der Mailadresse über ein Double Opt-In Verfahren</li>
<li>Wähle für jeden verifizierten Benutzer aus dem Pool aller Rabattcodes sechs freie Codes aus</li>
<li>Erstelle für jeden dieser Codes einen QR Code</li>
<li>Erstelle auf Basis der sechs QR Codes ein PDF Dokument für den Benutzer und sende es ihm per Mail</li>
</ul>
<p>Hinzu kamen nicht-funktionale Anforderungen. Um in der kurzen Projektphase stets zeitnah und zuverlässig auf Detailänderungen in den funktionalen Anforderungen reagieren zu können, war eine hohe Testabdeckung erforderlich. Weiterhin sollten Änderungen immer umgehend in der Produktionsumgebung verfügbar sein, damit eine enge Feedback-Schleife mit den Anforderern möglich war. Dies wiederum bedingte eine vollautomatische Continuous Delivery Pipeline, und eine der Voraussetzungen hierfür war der Einsatz von Datenbank-Migrations.</p>
<p>Da das System personenbezogene Daten speichern würde, wurde eine externe Sicherheitsüberprüfung eingeplant, und diese mit einem guten Ergebnis zu bestehen war eine weitere Anforderung. Zusätzlich war das Thema Laststabilität im Fokus - zwar wurde die Anwendung nur einem begrenzten Nutzerkreis zur Verfügung gestellt, aber da es sich um eine zeitlich eng begrenzte Sonderaktion handelte, war ein gewisser Ansturm zu Beginn der Aktion zumindest möglich. Daher wurde auch ein Lasttest eingeplant mit der Anforderung, dass die Webanwendung auch bei vielen parallelen Zugriffen gute Antwortzeiten lieferte.</p>
<p>Eine weitere nicht-funktionale Anforderung war, dass die Anwendung auch auf mobilen Geräten angenehm zu bedienen sein sollte.</p>
<h2 id="die-umsetzung">Die Umsetzung</h2>
<h3 id="aufsetzen-des-projekts">Aufsetzen des Projekts</h3>
<blockquote>
<p>Die <a href="https://github.com/Galeria-Kaufhof/goodbuy-metro#good-buy-metro">README des Projekts</a> auf GitHub bietet einen Leitfaden zur Einrichtung eines Mac OS X Systems als Entwicklungsumgebung für die Anwendung.</p>
</blockquote>
<p>Der erste Schritt in der Entwicklung war das Anlegen eines neuen Symfony2 Projekts. Ich entschied mich für die aktuelle stabile nicht-LTS Version von Symfony, zum damaligen Zeitpunkt 2.7.3. So verständlich ich die Idee von Long Term Support Versionen finde, ziehe ich dennoch vor, lieber immer mit einer aktuellen stabilen Version zu arbeiten und auch immer zeitnah (vielleicht nach 2-3 minor releases) auf eine neue stabile Version upzugraden, wenn diese verfügbar wird.</p>
<p>Meiner Meinung nach läuft man ein eine Falle wenn man zu lange auf einer älteren Version verharrt, ein Vorgehen, welches durch LTS Versionen begünstigt wird. Man verliert einfach den Anschluss und ein Wechsel, der ja irgendwann erfolgen <em>muss</em>, wird immer furcheinflößender, komplexer und teurer. Lieber regelmäßig durch einen kleinen Schmerz gehen (der bei guter Testabdeckung eh überschaubar ist) und nicht in die Falle laufen, irgendwann ein Legacy-System zu haben. Für mich ist dieses Vorgehen ein Beispiel für das agile Prinzip <em>If It Hurts, Do It More Often</em> - guten Lesestoff bietet hier zum Beispiel Martin Fowler in <a href="http://martinfowler.com/bliki/FrequencyReducesDifficulty.html">FrequencyReducesDifficulty</a>.</p>
<p>Wie unter <a href="http://symfony.com/doc/current/book/installation.html">Installing and Configuring Symfony</a> beschrieben wurde der Symfony Installer heruntergeladen und installiert, um dann mittels <code class="highlighter-rouge">symfony new goodbye-metro 2.7.3</code> das Projekt aufzusetzen.</p>
<p>Symfony2 ist die Basis der Anwendung im Backend, aber eine Webanwendung hat auch ein Frontend, und auch dieses will z.B. in Hinblick auf externe Bibliotheken und Frameworks gemanaged werden. Hierzu wurde <em>Bower</em>, der JavaScript Paketmanager, benutzt. Über die Datei <em>bower.json</em> im Hauptverzeichnis des Projekts wurde <em>Bootstrap</em> als Abhängigkeit definiert:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"name": "goodbye-metro",
"version": "0.0.1",
"dependencies": {
"bootstrap": "~3.3.5"
}
}
</code></pre></div></div>
<p>Um Bower und Symfony2 sinnvoll zu integrieren ist es wichtig dafür zu sorgen, dass Bower seine Bibliotheken im richtigen Zielverzeichnis ablegt. Der Symfony Best Practice folgend, sollte die Anwendung im Bundle <em>AppBundle</em> entstehen. Die öffentlichen Webdateien für dieses Bundle gehören in <em>src/AppBundle/Resources/public</em> - über das Assetsystem von Symfony wird dieser Ort nach <em>web/bundles/app</em> gespiegelt, und von dort können die Dateien vom Webserver geserved werden. Da wir mit Bower externen Code in unser Projekt holen (analog zu den externen PHP Libraries, die mittels Composer in <em>vendor</em> im Wurzelverzeichnis des Projekts landen), macht es Sinn auch diese in einem <em>vendor</em> Ordner abzulegen, um sie nicht mit internen Frontend-Dateien zu vermischen.</p>
<p>Um dies zu erreichen, wurde die Datei <em>.bowerrc</em> mit folgendem Inhalt angelegt:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"directory": "src/AppBundle/Resources/public/vendor",
"interactive": false
}
</code></pre></div></div>
<p><code class="highlighter-rouge">"interactive": false</code> ist nützlich, um Bower ausführen zu können ohne dass Eingaben an der Kommandozeile abgefragt werden.</p>
<p>Wichtiges Detail: die externen Bibliotheken, die per Bower gemanaged werden, sollen nicht Teil des git Repositories werden. Daher wurde die Zeile <code class="highlighter-rouge">src/AppBundle/Resources/public/vendor</code> zur <em>.gitignore</em> Datei hinzugefügt.</p>
<p>Die Dependencies der PHP Welt hat der Symfony Installer automatisch nach <em>vendor/</em> heruntergeladen. Für die Frontend Dependencies müssen wir mittels Bower selber tätig werden:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bower install
</code></pre></div></div>
<h3 id="migrations-als-grundlage-für-continuous-delivery">Migrations als Grundlage für Continuous Delivery</h3>
<p>Damit war nun ein Grundgerüst für die zu bauende Anwendung, sowohl in Hinblick auf das Backend als auch das Frontend, verfügbar. Aber dieses Grundgerüst musste noch erweitert werden, um den zukünftigen Anforderungen gerecht zu werden.</p>
<p>Ein ganz zentrales Element für das Erreichen einer Continuous Delivery sind Datenbankmigrationen. Statt händisch Schemaänderungen vorzunehmen, sind Veränderungen an der Struktur einer Datenbank abgebildet in Codedateien, die Teil des Projektrepositories sind wie anderer Code auch. Das Schema der Datenbank ist somit einerseits versioniert, andererseits können Schemaänderungen ohne menschliches Zutun durchgeführt werden.</p>
<p>Ist dieses Verfahren aufgesetzt, kann neuer Code automatisiert auf die Produktionsumgebung ausrollen, selbst wenn dieser Code eine veränderte Datenbankstruktur erwartet - im Zuge des Ausrollens wird die Datenbank automatisch auf die Struktur angepasst, die der neue Code erwartet.</p>
<p>Datenbankmigrationen sind in Symfony2 Projekten sehr leicht zu realisieren, da hierfür ein entsprechendes Bundle existiert. Um dieses zu installieren (und automatisch zu den Composer-verwalteten externen Abhängigkeiten hinzuzufügen), reicht folgender Aufruf:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>composer require doctrine/doctrine-migrations-bundle "^1.0"
</code></pre></div></div>
<p>Das neue Bundle musste nun dem Kernel der Anwendung bekannt gemacht werden, indem <em>app/AppKernel.php</em> um den Eintrag</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle()
</code></pre></div></div>
<p>erweitert wurde, und die folgenden Konfigurationsparameter mussten in <em>app/config/config.yml</em> hinzugefügt werden:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>doctrine_migrations:
dir_name: "%kernel.root_dir%/DoctrineMigrations"
namespace: Application\Migrations
table_name: migration_versions
name: Application Migrations
</code></pre></div></div>
<p>Um nun zu ersten Migrations zu kommen machte es Sinn, eine erste Entität zu schaffen und die Erzeugung der dazugehörigen Datenbankstruktur in einer ebensolchen Migration abzubilden. Symfony2 bietet alle Hilfsmittel um diesen Weg nicht komplett zu Fuß gehen zu müssen.</p>
<p>Der naheliegenste Kandidat für diese erste Entität war der Nutzer der Rabattaktion, intern <em>Customer</em> genannt - die Namensgebung <em>User</em> oder, da es sich grundsätzlich im Konzernmitarbeiter handelte, <em>Employee</em>, wäre sicherlich ebenfalls möglich gewesen.</p>
<p>Über <code class="highlighter-rouge">php app/console doctrine:generate:entity</code> erfolgte die interaktive Erzeugung der Entität <em>Customer</em>. Das Ergebnis sieht man unter <a href="https://github.com/Galeria-Kaufhof/goodbuy-metro/blob/ff28bc6d1d9e823e17a5b153b1911527ef45aa55/src/AppBundle/Entity/Customer.php">src/AppBundle/Entity/Customer.php auf GitHub</a>.</p>
<p>Direkt mit Bordmitteln gelangt man von der neuen Entität zur zugehörigen Migrations-Datei: <code class="highlighter-rouge">php app/console doctrine:migrations:diff</code> stellt die Unterschiede zwischen dem Code (dem die <em>Customer</em> Entität bereits bekannt ist) und der Datenbank (die noch keine zugehörige Tabelle kennt) fest und legt unter <em>app/DoctrineMigrations/</em> eine Datei mit entsprechenden SQL Statements an (siehe <a href="https://github.com/Galeria-Kaufhof/goodbuy-metro/blob/ff28bc6d1d9e823e17a5b153b1911527ef45aa55/app/DoctrineMigrations/Version20150828083456.php">app/DoctrineMigrations/Version20150828083456.php auf GitHub</a>).</p>
<p>Um die Datenbank nun mit dem Code zu synchronisieren, führt man schlicht <code class="highlighter-rouge">php app/console doctrine:migrations:migrate</code> aus.</p>
<blockquote>
<p>Hierbei sollte man beachten, dass neu erzeugte Migrations immer sofort angewendet werden sollten, bevor man weitere Veränderungen an Entitäten vornimmt. Der <em>diff</em> Befehl ist sehr gut darin zu erkennen, was die Unterschiede zwischen Entitäten und Datenbank sind, aber er kann nicht berücksichtigen, welche unangewendeten Migrations bereits existieren. Führt man zum Beispiel nach dem Erzeugen der Entität den <em>diff</em> Befehl zwei Mal direkt hintereinander aus, dann erhält man zwei Migrationsdateien, die aber bei den den gleichen Inhalt haben (Anlegen der Tabelle für die Entität), und ein Ausführen von <em>migrate</em> würde fehlschlagen wenn nach Anwenden der ersten Migrationsdatei die Anwendung der zweiten versucht, die soeben erstellte Tabelle noch mal anzulegen.</p>
</blockquote>
<h3 id="motivation-für-continuous-delivery">Motivation für Continuous Delivery</h3>
<p>Warum eigentlich noch mal das Ganze? Das Ziel ist die Schaffung und Nutzung einer Continuous Delivery Pipeline, und Migrations sind neben Tests ein notwendiges Mittel zum Zweck.</p>
<p>In der Softwareentwicklung bei Galeria.de ist Continuous Delivery ein sehr zentraler Baustein unseres Produktentwicklungsprozesses, deshalb wurde auch bei diesem sehr kleinen Sonderprojekt Wert darauf gelegt.</p>
<p>Wer Software entwickelt, kennt vermutlich das Phänomen: In der eigenen Entwicklungsumgebung funktioniert alles wie gewünscht, aber auf dem Produktionssystem verhält sich die Software anders, und nicht selten fehlerhaft. Oder auch: man ist zu 95% fertig mit dem Projekt, nun muss man es “nur noch” releasen, und stellt fest, dass man nicht 5%, sondern noch 30% des Aufwands vor sich hat, bis man wirklich gelauncht ist.</p>
<p>Das hervorragende Buch <em>Growing Object-Oriented Software, Driven By Tests</em> macht dieses Phänomen auf interessante Weise anschaulich. Würde man eine “Stresskurve” über die Projektdauer plotten die anzeigt, welches Level von Stress oder Chaos im Projekt zu einem beliebigen Zeitpunkt auf dem Weg zum Launch herrscht, dann sieht diese klassischerweise wie folgt aus:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Stress/Chaos
^
│
│ *
│ *
│ *
│ * *
│ * *
│ * *
│ ************************** *
+───────────────────────────────> Zeit
|
Launch
</code></pre></div></div>
<p>In der Zeit vor dem Launch ist es verhältnismäßig ruhig - man arbeitet vor sich hin, die Anwendung entsteht und lebt in der Entwicklungsumgebung, welche überschaubar und gut beherrscht ist. Dann kommt die Launchphase, und es wird hektisch: in Produktion sind Softwarepakete auf einem ganz anderen Stand, einzelne Nodes in der Entwicklungsumgebung werden zu Clustern mit vielen Nodes in Produktion, Netzwerkrouten funktionieren nicht, bei jedem Deployment ist die Seite einige Minuten lang offline und so weiter und so fort.</p>
<p>Eine Anwendung zu bauen hat aus dieser Perspektive zwei Aspekte: Das Herstellen von Funktionalität, und das Herstellen von Betriebsbereitschaft. Im klassischen Vorgehen liegt während nahezu der gesamten Projektphase der Fokus fast ausschließlich auf dem ersten Aspekt, und die Betriebsbereitschaft kommt zu kurz. Continuous Delivery dreht den Spieß um:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Stress/Chaos
^
│
│
│
│
│ *
│ *
│ * *
│ ************************ **
+───────────────────────────────> Zeit
|
Launch
</code></pre></div></div>
<p>Bei diesem Ansatz werden die knackigen Herausforderungen, die Software betriebsbereit zu bekommen und einen funktionierenden und zuverlässigen Releaseprozess sicherzustellen, an den Projektanfang gesetzt (<em>“Do the hard stuff first”</em>). Das ist durchaus anstrengend, denn es dauert in der Regel einen Moment bis zum allerersten Mal die (zu diesem Zeitpunkt in ihrer Funktionalität und Komplexität natürlich noch äußerst rudimentäre) Software sauber bis zu den Produktionssystemen ausrollt - dabei will man doch “richtig loslegen” und Features bauen, statt sich jetzt schon mit dem Aufsetzen von Serversystemen auseinanderzusetzen.</p>
<p>Aber hat man diese Hürde erst einmal genommen, erntet man für die gesamte Lebensdauer der Anwendung, und ganz besonders in der Launchphase, die Früchte dieses Ansatzes. Der finale Release, d.h. das zur-Verfügung-stellen der Anwendung für den eigentlichen Kunden, ist zum Zeitpunkt des Launches bereits dutzende, wenn nicht hunderte Male geübt und eingespielt. Wenn man ein Feature fertiggestellt hat, ist es <em>wirklich</em> fertig: Es liefert die geforderte Funktionalität, <em>und</em> rollt zuverlässig zum Kunden aus, <em>und</em> funktioniert auch im Live-Betrieb. Eine Funktionalität, die letzteres nicht bietet, ist aus Kundensicht exakt so wertvoll wie die vollständige Abwesenheit der Funktionalität.</p>
<p>Continuous Delivery ist dabei natürlich kein singuläres Event - so, wie die Anwendung in der Projektphase in ihrer Funktionalität wächst, wächst auch der Deliveryprozess mit. Hier ist man nicht davor gefeit, ab und zu eine kleine Überraschung zu erleben und nachbessern zu müssen - aber meiner Erfahrung nach lebt es sich deutlich besser mit seltenen kleineren Überraschungen als mit einer großen zum ungünstigsten denkbaren Zeitpunkt, dem Launch.</p>
<h3 id="erste-tests">Erste Tests</h3>
<p>Zurück zur Anwendung. Migrations waren nun aufgesetzt, und eine funktionierende Continuous Delivery das nächste Ziel. Um dies nicht ganz im luftleeren Raum zu verfolgen, ging es nun darum, erste Funktionalität zu erzeugen - und die Korrektheit dieser Funktionalität mit einem Testfall zu beweisen, denn die Idee einer Continuous Delivery Pipeline ist ja, dass sie automatisch, ohne weiteren menschlichen Eingriff, die Software auf Produktionssystemen veröffentlicht; da also auch kein Mensch die Korrektheit testet, muss die Korrektheit über automatisierte Testfälle gewährleistet werden.</p>
<p>Zu diesem Zeitpunkt existierte lediglich die <em>Customer</em> Entität, und diese verfügte nicht wirklich über nennenswertes Verhalten, welches sinnvoll zu testen gewesen wäre. Der nächste Schritt war daher die Schaffung eines ersten Testfalls, der relevantes Verhalten der Anwendung überprüfen würde, und der innerhalb eines Delivery-Durchlaufs bewies, dass die Anwendung auf dem Zielsystem erwartungsgemäß funktionierte. Der Fokus lag daher auch auf einem funktionalen Test, und nicht auf einem Unit-Test; Units wie Methoden und Klassen sind in der Regel so isoliert, dass ihr Funktionieren innerhalb eines Testcases wenig darüber aussagt, ob die Anwendung an sich auf dem Zielsystem korrekt läuft - sprich, selbst eine ganze Batterie an fehlerfrei durchlaufenden Unittests sagt mir nicht, ob meine Continuous Delivery eine für den Benutzer korrekt laufende Anwendung zum Ergebnis hat.</p>
<p>Funktionale Tests zu schreiben ist in Symfony2 Anwendungen glücklicherweise sehr einfach und komfortabel. Man testet hierbei zwar nicht auf Basis realer HTTP-Anfragen und -Antworten, aber man testet dennoch die integrierte Anwendung in ihrer Gesamtheit und kann somit sicherstellen, dass sich alle relevanten Komponenten im Zusammenspiel korrekt verhalten.</p>
<p>Um funktionale Tests schreiben zu können, bedurfte es nur wenig Vorbereitung. Zum einen musste PHPUnit als Dependency definiert werden mittels <code class="highlighter-rouge">require phpunit/phpunit "^4.8"</code>, und eine <em>phpunit.xml.dist</em> musste im Wurzelverzeichnis des Projekts angelegt werden - siehe <a href="https://github.com/Galeria-Kaufhof/goodbuy-metro/blob/master/phpunit.xml.dist">phpunit.xml.dist auf GitHub</a> für den Inhalt.</p>
<p>Nun kann man über das Schreiben von Testklassen, die <em>Symfony\Bundle\FrameworkBundle\Test\WebTestCase</em> erweitern, funktionale Testfälle erzeugen. Der allererste funktionale Testfall im Projekt, in Datei <em>src/AppBundle/Tests/Functional/RegistrationTest.php</em>, sah wie folgt aus:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><?php
namespace AppBundle\Tests\Functional;
use AppBundle\Tests\TestHelpers;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class RegistrationTest extends WebTestCase
{
public function testContents()
{
$client = static::createClient();
$client->request('GET', '/');
$this->assertEquals(200, $client->getResponse()->getStatusCode());
}
}
</code></pre></div></div>
<p>Der Testfall selber ist sehr überschaubar, aber sein Funktionieren beweist, dass die integrierte Anwendung in der Lage ist, korrekt auf einen Request gegen die Route / zu antworten. Wie gesagt werden hierbei keine realen HTTP Requests über eine reale Leitung geschickt - der <em>$client</em>, den man über den <em>WebTestCase</em> von Symfony2 erzeugt, ist lediglich eine clevere Abstraktion, die stets im Kontext der PHP Laufzeit bleibt. Jedoch läuft der Client gegen die vollständig integrierte Symfony-Anwendung, d.h. der Testfall kann nur erfolgreich sein, wenn Dependencies, Konfiguration, Routing, Controller, Datenbank usw. richtig funktionieren und zusammenspielen. Für das angestrebte Ziel ist dies völlig ausreichend.</p>
<p>Ausgeführt wird dieser Testfall nun schlicht mittels <code class="highlighter-rouge">php ./vendor/phpunit/phpunit/phpunit</code>.</p>
<p>An diesem Punkt war ein wichtiger erster Zwischenstand erreicht: Die Anwendung war grundsätzlich aufgesetzt, Veränderungen an der Datenbank waren dank Migrations codeseitig steuerbar, und die notwendigen Strukturen für einen ersten Testcase waren in Stellung gebracht. Mit anderen Worten: Das erste Paket für die Delivery war geschnürt - nun brauchte es die Pipeline zum Produktivsystem, über die das Paket geliefert werden konnte.</p>
<blockquote>
<p>Im demnächst erscheinenden Teil 2 dieser Serie wird der Aufbau der Continuous Delivery Pipeline in allen Details beleuchtet.</p>
</blockquote>manuelkiesslingÜber diesen ArtikelA “frontend middleware” on top of a shared-nothing architecture2015-07-28T00:00:00+00:002015-07-28T00:00:00+00:00/general/2015/07/28/abstracting-the-frontend-in-a-shared-nothing-architecture-driven-by-functional-teams<h2 id="the-challenge">The challenge</h2>
<p>Before I explain what exactly a <em>“frontend middleware”</em> is, let me first tell you why we invented it and why it might make sense for you to use one, too.</p>
<p>About one year ago, when I joined the e-commerce team at GALERIA Kaufhof working on their new webshop platform, everything was still in its early stages. The idea of having totally separated application “verticals” was the crazy new thing and the entire system was cut into loosely coupled services to keep the business logic separated. From a backend and business perspective this perfectly made sense.</p>
<blockquote>
<p>See our <a href="http://www.inoio.de/blog/2014/09/20/technologie-sprung-bei-galeria-kaufhof/">Jump - Ein Technologie-Sprung bei Galeria Kaufhof</a> post (in German) for background information on the new webshop platform architecture.</p>
</blockquote>
<p>But I realized very quickly that - from a frontend view - having five entirely separated functional teams working on things like integrating tracking solutions, affiliates or any other third-party content wasn’t very efficient. Five independent frontend devs had to individually learn tracking APIs, implement tracking snippets, manage things like accounts within their own application’s context, and so on. You should get the point. Sounded like the same job done multiple times by multiple people.</p>
<h2 id="wait-whats-the-problem-again">Wait, what’s the problem again?</h2>
<p>If you are not a frontend developer you might wonder why we didn’t simply embed tracking and retargeting pixels into the markup, just as the vendors tell us. Let me explain. Directly embedding third-party code may introduce two major problems: performance issues and script errors. You might know that all script tags in the markup are synchronously loaded and blocking the pageload by default. Although most vendors switched to asynchronously loading tags (using the <a href="http://caniuse.com/#feat=script-async"><code class="highlighter-rouge">async</code> attribute</a>), synchronous loading can cause enormous performance penalties if you’re not aware of the problem. Additionally there is always a risk that broken third party scripts cause Javascript errors, which - in the worst case - break and halt your entire script logic.</p>
<p>Besides the common performance issues and risk of errors, directly embedding third parties is also horribly inefficient and unscalable. Agreed - for one single developer, placing one global pixel in the head of the outmost template in his blog, such approach might be sufficient. But as soon as you start scaling, things become more and more unmaintainable. Imagine you split the application frontend among five or more teams and try to include 10 to 20 pixels. Each team needs to have a story (or at least some sort of task or ticket if you’re not into Scrum) for adding, editing or removing every single pixel. Add project management costs on top and it quickly becomes really expensive.</p>
<p>Even worse - having no dedicated owner for the integration of third parties means there is nobody to ensure that the technical integration is done correctly or in any way consistent among the teams. Not to mention that, in the worst case, you need a complete application deployment (involving all verticals) to update or change a single pixel. I hope this illustrates the problem.</p>
<h2 id="the-data-layer-approach">The “data layer” approach</h2>
<p>Working with tag management systems before, I got used to what I’d call the “data layer” approach pretty well. To put it simple: the website renders some kind of interesting business information (e.g. product attributes or shopping basket contents) into its page body. Often this is done using some Javascript API. The tagmanager software then takes this information and hands it over to third-party tools like analytics, affiliates or alike. A very simple example, illustrating the concept:</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="c1">// common example of a "data layer"</span>
<span class="kd">var</span> <span class="nx">DataLayer</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">DataLayer</span> <span class="o">||</span> <span class="p">[];</span>
<span class="nx">DataLayer</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
<span class="na">product</span><span class="p">:{</span>
<span class="na">id</span><span class="p">:</span> <span class="mi">1234567890</span><span class="p">,</span>
<span class="na">name</span><span class="p">:</span> <span class="s2">"Book"</span><span class="p">,</span>
<span class="na">price</span><span class="p">:</span> <span class="mf">19.95</span>
<span class="p">}</span>
<span class="p">});</span></code></pre></figure>
<p>This technique felt like a sensible solution for our architecture of rather strictly separated components. Each vertical application could independently render data into the markup, and some frontend logic would consume this data and take care of the rest (i.e. dispatching data to third parties). While thinking about it, we wanted to take it even one step further and do fancy things like declarative tracking, channel recognition and tag management within our own context.</p>
<h2 id="meta-tags-to-the-rescue">Meta tags to the rescue</h2>
<p>From a technical perspective things were pretty obvious. We needed some API that the application could utilize for transporting information into the client space. Also, the implementation had to be completely generic and shouldn’t force any of the structural decisions (i.e. “verticals”) upon the API. An additional scripting API to call from within custom controllers would be nice, but wasn’t a top priority.</p>
<p>Being modern frontend guys, we always avoid inlined script tags whenever possible. So we decided to not rely on Javascript for passing the data around. Instead, we are using dedicated metatags containing JSON data for handing over information from the markup to our <em>frontend middleware</em> (which we decided to call “Data Abstraction Layer”, or DAL). An example metatag could look like this:</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="c"><!-- example for a page data object --></span>
<span class="nt"><meta</span> <span class="na">name=</span><span class="s">"gk:dal:data"</span> <span class="na">content=</span><span class="s">'{
"page":{
"type":"homepage",
"name":"Startseite"
}}'</span> <span class="nt">/></span></code></pre></figure>
<p>This also gave us the important advantage that each vertical application could independently render any kind of data anywhere into a page’s markup (even within SSI contexts) without worrying about availability or load order of a library or API. Markup reliably loads before any scripts. For asynchronously loaded content we developed another, Javascript-driven solution based on broadcasting events top-down from the DAL to its plugins. I’ll talk about that later.</p>
<h2 id="plugins-rules-and-repos">Plugins, Rules and Repos</h2>
<p>Behind the scenes and concepts the DAL is pretty lightweight and straightforward. From a technical perspective it is just an advanced plugin loader, lazy loading its plugins using <em>requirejs</em>. On initialization it collects all metatags from the page, aggregates the contained data into one large object, loads its plugins depending on supplied rules and hands over data to each of them. The more specific magic is then going on inside each plugins’ code.</p>
<p>The plugins are standard AMD-modules returning a class with at least a constructor and a <code class="highlighter-rouge">handleEvent</code> method. When instantiated, the constructor receives a reference to the DAL module, the aggregated page, and an optional configuration object. The <code class="highlighter-rouge">handleEvent</code> method does all the magic required for handling asynchronous events happening within a page’s lifecycle. A simple stub plugin could look like the following code (this time in <a href="http://coffeescript.org">Coffescript</a>):</p>
<figure class="highlight"><pre><code class="language-coffeescript" data-lang="coffeescript"><span class="nx">define</span> <span class="s">"gk/lib/dal/demoAffiliate"</span><span class="p">,</span> <span class="p">[</span><span class="s">"thirdpartylib"</span><span class="p">]</span> <span class="p">(</span><span class="nx">thirdpartyLib</span><span class="p">)</span> <span class="o">-></span>
<span class="c1"># A simple demo plugin. Just provides a class container with example plugin logic</span>
<span class="c1"># @implements IDALService</span>
<span class="k">class</span> <span class="nx">DemoAffiliate</span>
<span class="c1"># init our third party lib</span>
<span class="na">constructor</span><span class="o">:</span> <span class="p">(</span><span class="vi">@</span><span class="na">dal</span><span class="p">,</span> <span class="vi">@</span><span class="na">data</span><span class="p">,</span> <span class="vi">@</span><span class="na">config</span><span class="p">)</span>
<span class="nx">thirdpartyLib</span><span class="p">.</span><span class="na">init</span><span class="p">(</span><span class="s">"someaccountid"</span><span class="p">)</span>
<span class="c1"># handle async events</span>
<span class="na">handleEvent</span><span class="o">:</span> <span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">data</span><span class="p">,</span> <span class="nx">domain</span><span class="p">)</span> <span class="o">-></span>
<span class="k">if</span> <span class="nx">name</span> <span class="o">is</span> <span class="s">"addtocart"</span><span class="o">:</span>
<span class="c1"># notify my third-party backend about this event</span>
<span class="nx">thirdpartyLib</span><span class="p">.</span><span class="na">notify</span><span class="p">(</span><span class="s">"addtocart"</span><span class="p">,</span> <span class="nx">data</span><span class="p">.</span><span class="na">product</span><span class="p">.</span><span class="na">id</span><span class="p">)</span>
</code></pre></figure>
<p>The plugin loading is based on a simple ruleset which, optionally, executes callbacks before loading a plugin. That allows for complex rule logic, which no tagmanager could offer out-of-the-box (e.g. <em>if the page’s type is “checkout-complete”, the user is logged in, and has more than 3 articles in his basket</em>). This even enables us to eventually replace our external tagmanager and host the entire affiliate integration right within our own git repository. If you are a developer and/or you ever worked with external tag management GUIs (or any other, less comfortable form of affiliate integration) you might know what a great relief that is. We are even able to <strong>fully unit test our affiliate pixels, integrated within our CD pipeline</strong>.</p>
<h2 id="going-async">Going async</h2>
<p>Most modern websites don’t involve a new pageload for every action. Actions like e.g. opening a layer, expanding some accordion or using the off-canvas navigation may happen anytime, asynchronously, without our DAL ever being notified. For such cases we developed the <code class="highlighter-rouge">DAL.broadcast</code> mechanism. It offers a simple, one-way message API that allows sending event notifications directly to the DAL. Whenever a script causes an asynchronous action that should be globally broadcasted, the <code class="highlighter-rouge">DAL.broadcast</code> method can be called with the specific event name and an optional information object:</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="c1">// broadcast a client event to the DAL</span>
<span class="nx">DAL</span><span class="p">.</span><span class="nx">broadcast</span><span class="p">(</span><span class="s2">"product-addtocart"</span><span class="p">,</span> <span class="p">{</span>
<span class="na">id</span><span class="p">:</span> <span class="s2">"1234567ABC"</span><span class="p">,</span>
<span class="na">name</span><span class="p">:</span> <span class="s2">"FABIANI Jeans"</span><span class="p">,</span>
<span class="na">price</span><span class="p">:</span> <span class="mf">19.95</span>
<span class="p">});</span></code></pre></figure>
<p>While this solved the issue of being notified about asynchronous events it introduced a new problem. We now needed to write specific controllers for any element that should fire an event. Having to write a dedicated controller for any single button actually felt quite ugly and would have caused tons of useless code. So we decided to make the tracking more declarative and introduced custom attributes that allowed to track events without additional controller logic.</p>
<p>We identified three common types of client events we are usually interested in: <em>click/touch</em>, <em>view</em> and <em>focus</em>. These event types can be automatically handled and applied to the appropriate logic using the custom data-attribute syntax <code class="highlighter-rouge">data-dal-event-{type}</code>. This would also allow for future extensibility (thinking of <em>swipe</em>, <em>pinchzoom</em>, …). The following examples illustrate the basic principle:</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="c"><!-- simple click event without custom data --></span>
<span class="nt"><button</span>
<span class="na">name=</span><span class="s">"loginLayerOpen"</span>
<span class="na">data-dal-event-click=</span><span class="s">'{"name":"layer-login-open"}'</span>
<span class="nt">/></span></code></pre></figure>
<p>Using this <em>declarative tracking</em> it was now easily possible to apply tracking logic to elements without touching anything but the markup. For more advanced use cases it is also possible to append additional data directly to the event data, following the same type definition and argument signature we use for the <code class="highlighter-rouge">DAL.broadcast</code> function itself.</p>
<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><span class="c"><!-- click event with custom data (a teaser id in this case) --></span>
<span class="nt"><div</span>
<span class="na">class=</span><span class="s">"my-teaser"</span>
<span class="na">data-dal-event-click=</span><span class="s">'{
"name":"teaser-click",
"data":{
"teaserId":"my-fashion-teaser",
"campaign":"some/fancy/campaign"
}
}'</span>
<span class="nt">/></span></code></pre></figure>
<h2 id="standards-and-conventions-ftw">Standards and conventions FTW</h2>
<p>At this point you might ask: <em>“Yeah, sounds nice, but what’s all the buzz about? This doesn’t look like rocket science!”</em> Agreed (mostly). But, that’s only a small part of the cake. The majority of work went into defining conventions and standards for the various types and use cases. Which data do we have to pass to our DAL? What are the globally required fields? How do we name our pages? Which data do we need for which event and/or page? What are the more specific bits of information each vertical had to pass?</p>
<p>The answers to all those questions are very subjective and closely related to the field of business. Obviously, for e-commerce sites you have substantially different information you want to collect, compared to a content-driven online magazine. But in any case it boils down to some key metrics you want to collect and analyze for your business. E.g., a big part of the DAL’s data is passed to our RUM (Real User Monitoring) and BI (Business Intelligence) software, which are also implemented as DAL plugins. Thus, many of our own conventions and metrics are specific to this use case. Another major part is the affiliate and basket tracking integration.</p>
<p>Luckily, I had done most of that long time before, when I initiated the project “tagmanager integration” for our old webshop. So I started writing down the important KPIs and metrics based on that historical data and the tracking-pixels from our tracking solution and our recommendation engine. I summed it all up in a table in our wiki, structured the table based on verticals and added a description for all keys. Then I wrote the integration stories for the vertical teams so they could integrate the right metatags and declarations in the markup.
Awesome, or so I thought.</p>
<p>Well, it felt awesome until I looked at the actual implementation done by the teams. The problem was that I had not been very explicit in declaring the types (and especially the formatting) for the individual metrics. I simply defined something like <em>on the product detail page we need a product object with the following fields: name, price, category, …</em>. Even though I also defined some examples, this still led to fairly diverse implementations. Especially the price formatting was a problem, because there were multiple interpretations about how a price should be expressed (e.g.<code class="highlighter-rouge">"29,90€"</code> vs. <code class="highlighter-rouge">29.9</code> which are both valid representations of the same price value).</p>
<h2 id="strong-typing-to-the-rescue">Strong typing to the rescue</h2>
<p>Since Javascript (and JSON, too) is a very loosely typed language, we needed to define abstract types (or interfaces if you’re using <a href="http://typescriptlang.org">Typescript</a>) that explicitly define how values have to be supplied to the DAL. This resulted in various fancy types, e.g. <em>DALPageData</em>, <em>DALUserData</em> or <em>DALProductData</em> to just name a few. Let’s look at a part of the type definition for the <em>DALProductData</em> type:</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="c1">// @interface IDALProductData</span>
<span class="nx">DALProductData</span> <span class="o">=</span> <span class="p">{</span>
<span class="cm">/**
* Internal ID of this product.
*/</span>
<span class="s2">"productId"</span> <span class="p">:</span> <span class="p">{</span>
<span class="s2">"type"</span> <span class="p">:</span> <span class="s2">"String"</span><span class="p">,</span>
<span class="s2">"mandatory"</span> <span class="p">:</span> <span class="kc">true</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="cm">/**
* EAN (International Article Number, see https://en.wikipedia.org/wiki/International_Article_Number_%28EAN%29) of this product.
*/</span>
<span class="s2">"ean"</span> <span class="p">:</span> <span class="p">{</span>
<span class="s2">"type"</span> <span class="p">:</span> <span class="s2">"String"</span><span class="p">,</span>
<span class="s2">"mandatory"</span> <span class="p">:</span> <span class="kc">true</span>
<span class="p">},</span>
<span class="cm">/**
* Object with price information.
*/</span>
<span class="s2">"priceData"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"type"</span><span class="p">:</span> <span class="s2">"DALPriceData"</span><span class="p">,</span>
<span class="s2">"mandatory"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="s2">"apiVersion"</span><span class="p">:</span> <span class="mi">2</span>
<span class="p">},</span>
<span class="cm">/* ... */</span>
<span class="p">}</span></code></pre></figure>
<p>As you can see we provide a JSON structure defining the type and some other, optional fields (e.g. <em>mandatory</em>, <em>deprecated</em>, <em>apiVersion</em> and some more). This way we ensure backwards compatibility throughout the API because the teams can safely adapt to new API versions without introducing breaking changes.</p>
<p>When looking at the <em>priceData</em> attribute, you might notice how we used the <em>DALPriceData</em> type to solve the previously mentioned issues with the price formatting. Also, instead of just declaring price-related attributes directly within <em>DALProductData</em>, we defined our dedicated type for generic price information that we use for products, carts, orders and anything else that might come in the future. Here is an excerpt from the <em>DALPriceData</em> type:</p>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="c1">// @interface IDALPriceData</span>
<span class="nx">DALPriceData</span> <span class="o">=</span> <span class="p">{</span>
<span class="cm">/**
* Current net price of the product or cart; *excluding* VAT, shipping or discount.
*/</span>
<span class="s2">"net"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"type"</span><span class="p">:</span> <span class="s2">"float"</span><span class="p">,</span>
<span class="s2">"mandatory"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="s2">"apiVersion"</span><span class="p">:</span> <span class="mi">2</span>
<span class="p">},</span>
<span class="cm">/**
* VAT part of 'net' price.
*/</span>
<span class="s2">"VAT"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"type"</span><span class="p">:</span> <span class="s2">"float"</span><span class="p">,</span>
<span class="s2">"mandatory"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="s2">"apiVersion"</span><span class="p">:</span> <span class="mi">2</span>
<span class="p">},</span>
<span class="cm">/**
* Total price (= net + VAT - discount); *including* VAT and *after* subtracting discount.
*/</span>
<span class="s2">"total"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"type"</span><span class="p">:</span> <span class="s2">"float"</span><span class="p">,</span>
<span class="s2">"mandatory"</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="s2">"apiVersion"</span><span class="p">:</span> <span class="mi">2</span>
<span class="p">},</span>
<span class="cm">/* ... */</span>
<span class="p">}</span></code></pre></figure>
<p>The curious reader might ask: <em>“Hey, what’s the purpose of defining these as Javascript objects?”</em> Well, within the metatags the data is supplied as plain object literals. But in our test infrastructure (based on Selenium) we can now run over the entire page, take the above type definitions and compare them to the actual implementation. If there are mismatches, e.g. because a vertical didn’t implement a type correctly, we raise an error and get a notification.</p>
<h2 id="conclusion">Conclusion</h2>
<p>So, finally, what is a “frontend middleware”? I introduced this term to describe a software architecture that “sits” between client and third-party space and provides a complete abstraction between those two. It replaces common tagmanagers, affiliate pixels and alike. At the same time it provides a declarative tracking API and deep integration with real-user monitoring, web analytics and other frontend-only tools commonly served by third parties (surveyforms, promolayers, onsite A/B testing, etc.). It is designed with verticalized, shared-nothing architectures, distributed over multiple functional teams in mind. Nevertheless it should also play really nice with any classic, monolithic architecture.</p>
<p>Thanks for your attention!</p>
<p><em>Wait - you actually read this far? Wow. You’re either seriously bored or might be really into this stuff. Did you know <strong>we are still looking for talented frontend devs</strong>? <a href="http://kaufhof-jobs.dvinci.de/cgi-bin/appl/selfservice.pl?action=jobdetail;job_pub_nr=F903080B-4E8E-4FBF-A284-9D1DDA8EC0DB;p=homepage">Get in touch</a> for more information.</em></p>ricopfausThe challenge Before I explain what exactly a “frontend middleware” is, let me first tell you why we invented it and why it might make sense for you to use one, too.GOTOnight Cologne on Microservices at Galeria.de Headquarters2015-06-22T00:00:00+00:002015-06-22T00:00:00+00:00/general/2015/06/22/gotonight-cologne-on-microservices-tilkov-traub-thomas<p><img style="border: 1px solid #eee;" src="/assets/images/2015-06-22-gotonight-cologne-on-microservices-tilkov-traub-thomas/gotonight-cologne-microservices-dennistraub-stefantilkov-davethomas-intro-presentation-thumbnail.png" />
<img style="border: 1px solid #eee;" src="/assets/images/2015-06-22-gotonight-cologne-on-microservices-tilkov-traub-thomas/gotonight-cologne-microservices-dennistraub-stefantilkov-davethomas-lineup-thumbnail.png" />
<img style="border: 1px solid #eee;" src="/assets/images/2015-06-22-gotonight-cologne-on-microservices-tilkov-traub-thomas/gotonight-cologne-microservices-dennistraub-stefantilkov-davethomas-dessert.png" />
<img style="border: 1px solid #eee;" src="/assets/images/2015-06-22-gotonight-cologne-on-microservices-tilkov-traub-thomas/gotonight-cologne-microservices-stefantilkov2.jpeg" /></p>
<p><br clear="all" /></p>
<p>On June 22, 2015, Galeria Kaufhof hosted a
<a href="http://gotocon.com/berlin-2015/freeevent/index.jsp?eventOID=7123">GOTOnight community event on Microservices</a>. Over 60
attendees listened to three great talks on the topic by <a href="https://twitter.com/DTraub">Dennis Traub</a>,
<a href="https://twitter.com/stilkov">Stefan Tilkov</a> and <a href="https://twitter.com/daveathomas">Dave Thomas</a>, and enjoyed the great
buffet that was provided by <a href="http://www.dinea.de/">Dinea</a>.</p>
<p>The talks were followed by a panel session with all three speakers, with lots of questions from the audience and a
lively discussion between Stefan, Dave and Dennis.</p>
<p>We have recorded a video of Dennis Traub’s talk <em>Taming the Monolith - Are Microservices just an implementation
detail?</em>. Note, however, that the video was made using the live streaming app
<a href="https://www.periscope.tv/">Periscope</a>, which seems to only allow videos in portrait mode, and includes comments by
people following the stream that are not affiliated with the event itself:</p>
<iframe width="420" height="315" src="https://www.youtube.com/embed/_85fg_9eXLQ" frameborder="0" allowfullscreen=""></iframe>
<p><br clear="all" /></p>
<p>The slides of Stefan Tilkov’s Talk <em>Microservices: Awesome, as long as they are neither ‘micro’ nor ‘services’</em>
are available, too:</p>
<script async="" class="speakerdeck-embed" data-id="ae63cff974a240d9959b47b8beae96a2" data-ratio="1.33333333333333" src="//speakerdeck.com/assets/embed.js"></script>
<p><br clear="all" /></p>
<p>The event also sparked quite some activity on Twitter:</p>
<blockquote class="twitter-tweet" lang="de"><p lang="en" dir="ltr">Last slide preparations by <a href="https://twitter.com/stilkov">@stilkov</a>. <a href="https://twitter.com/hashtag/GOTOnight?src=hash">#GOTOnight</a> <a href="https://twitter.com/hashtag/Cologne?src=hash">#Cologne</a> <a href="https://twitter.com/galeriakaufhof">@galeriakaufhof</a> <a href="http://t.co/nL0tTlIyvI">pic.twitter.com/nL0tTlIyvI</a></p>— Manuel Kiessling (@manuelkiessling) <a href="https://twitter.com/manuelkiessling/status/613011983403778048">22. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="de"><p lang="en" dir="ltr">The place is filling up at <a href="https://twitter.com/hashtag/GOTOnight?src=hash">#GOTOnight</a> Cologne <a href="http://t.co/0vLKqNRP6J">pic.twitter.com/0vLKqNRP6J</a></p>— Dennis Traub (@DTraub) <a href="https://twitter.com/DTraub/status/613012587249274880">22. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="de"><p lang="en" dir="ltr">The room’s packed at <a href="https://twitter.com/hashtag/GOTOnight?src=hash">#GOTOnight</a> Cologne // <a href="https://twitter.com/GOTOber">@GOTOber</a> <a href="http://t.co/26aR84ib4Z">pic.twitter.com/26aR84ib4Z</a></p>— Dennis Traub (@DTraub) <a href="https://twitter.com/DTraub/status/613039901978808320">22. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="de"><p lang="de" dir="ltr">Fantastischer Abend <a href="https://twitter.com/GOTONights">@GOTONights</a> mit <a href="https://twitter.com/DTraub">@DTraub</a>, <a href="https://twitter.com/stilkov">@stilkov</a> und <a href="https://twitter.com/daveathomas">@daveathomas</a>! Danke <a href="https://twitter.com/manuelkiessling">@manuelkiessling</a></p>— Wolfram Eberius (@eberius) <a href="https://twitter.com/eberius/status/613070248066084864">22. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="de"><p lang="en" dir="ltr">Thanks for hosting this cool event <a href="https://twitter.com/hashtag/gotonight?src=hash">#gotonight</a> <a href="https://twitter.com/manuelkiessling">@manuelkiessling</a>. It was really great to hear <a href="https://twitter.com/stilkov">@stilkov</a> and <a href="https://twitter.com/DTraub">@DTraub</a> "live und in farbe".</p>— Daniel Müller (@dotnetgeek) <a href="https://twitter.com/dotnetgeek/status/613081919241232385">22. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="de"><p lang="en" dir="ltr"><a href="https://twitter.com/stilkov">@stilkov</a> explains microservices at ou <a href="https://twitter.com/GOTONights">@GOTONights</a> in Cologne 🙌 <a href="http://t.co/yXSQeBNrZR">pic.twitter.com/yXSQeBNrZR</a></p>— Freek van Gool (@JavaFreekNL) <a href="https://twitter.com/JavaFreekNL/status/613031721668345856">22. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="de"><p lang="en" dir="ltr"><a href="https://twitter.com/DTraub">@DTraub</a> <a href="https://twitter.com/stilkov">@stilkov</a> Thanks for the great evening. You gave me some new food for thought. <a href="https://twitter.com/hashtag/Microservices?src=hash">#Microservices</a> <a href="https://twitter.com/hashtag/GOTOnight?src=hash">#GOTOnight</a></p>— Sascha Dittmann ☁ (@SaschaDittmann) <a href="https://twitter.com/SaschaDittmann/status/613231493796728832">23. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="de"><p lang="en" dir="ltr">"There's no ubiquitous language!" <a href="https://twitter.com/DTraub">@DTraub</a> at <a href="https://twitter.com/GOTONights">@GOTONights</a> Cologne <a href="https://twitter.com/hashtag/Microservices?src=hash">#Microservices</a> <a href="http://t.co/KYsH4xCeSJ">pic.twitter.com/KYsH4xCeSJ</a></p>— Giant Swarm (@giantswarm) <a href="https://twitter.com/giantswarm/status/613027503481974784">22. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="de"><p lang="en" dir="ltr">Great evening <a href="https://twitter.com/GOTONights">@GOTONights</a> in Cologne. With talks from <a href="https://twitter.com/stilkov">@stilkov</a> <a href="https://twitter.com/DTraub">@DTraub</a> and <a href="https://twitter.com/daveathomas">@daveathomas</a>. Thanks! <a href="https://twitter.com/hashtag/microservices?src=hash">#microservices</a></p>— Thomas Terkatz (@thomasterkatz) <a href="https://twitter.com/thomasterkatz/status/613087488878510080">22. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="de"><p lang="en" dir="ltr">A legend on stage! <a href="https://twitter.com/daveathomas">@daveathomas</a> giving the third talk at the <a href="https://twitter.com/hashtag/GOTOnight?src=hash">#GOTOnight</a> Cologne <a href="http://t.co/JMk6IOSdwl">pic.twitter.com/JMk6IOSdwl</a></p>— Dennis Traub (@DTraub) <a href="https://twitter.com/DTraub/status/613044094206615552">22. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="de"><p lang="en" dir="ltr">Full house in Cologne! /cc <a href="https://twitter.com/GOTONights">@GOTONights</a> <a href="https://twitter.com/kaesfiehs">@kaesfiehs</a> <a href="https://twitter.com/stilkov">@stilkov</a> <a href="https://twitter.com/DTraub">@DTraub</a> <a href="https://twitter.com/daveathomas">@daveathomas</a> <a href="http://t.co/zL4RrnHiH8">pic.twitter.com/zL4RrnHiH8</a></p>— Freek van Gool (@JavaFreekNL) <a href="https://twitter.com/JavaFreekNL/status/613023665672859649">22. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<blockquote class="twitter-tweet" lang="de"><p lang="en" dir="ltr">Final panel discussion at our <a href="https://twitter.com/GOTONights">@GOTONights</a> in Cologne. With <a href="https://twitter.com/stilkov">@stilkov</a> <a href="https://twitter.com/DTraub">@DTraub</a> and <a href="https://twitter.com/daveathomas">@daveathomas</a> <a href="http://t.co/80jpatfs4s">pic.twitter.com/80jpatfs4s</a></p>— Freek van Gool (@JavaFreekNL) <a href="https://twitter.com/JavaFreekNL/status/613064003615633408">22. Juni 2015</a></blockquote>
<script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script>manuelkiessling