This document describes a company's transition from a monolithic .NET application to a microservices architecture hosted on AWS. It details their use of a UI composition pattern using Nginx, ESI, and Jigsaw to compose fragments from different services into complete pages. This allows independent deployment of features while maintaining performance. Key aspects include caching of assets and responses, combining stylesheets and scripts, and isolation of services through separate CSS and JS packages.
4. Project Tatsu: Goals
4
• Attract talent
• Reduce time to market
• Release new features quickly (for test or production)
• Enable teams to innovate independently
5. Autonomous Teams, Loosely Coupled Services
5
Allow for cross-functional teams that are able to
independently create, improve, and run their services.
Avoid tight coupling as much as possible!
6. Don't Compromise Page Performance
6
• Achieve PageSpeed Insights score of 95+
• Strive for low latency
• Benefit from caching whereever possible
tricky to combine with
high team autonomy
9. API Gateway Pattern - Drawbacks
9
• No independent feature releases possible
• Web UI monolith
• API monolith
• Danger of adding more and more logic to the API layer
• Duplicated controller logic in the API gateway
12. Challenges of UI Composition
12
• Combine HTML
• Let services deliver their own styles and JavaScript
• HTML and asset versions must be consistent
• Page structure must not break page performance
• Request latency needs to stay low
• Independent and integration testing of UI components
15. ESI Include
15
<html>
<head>
<title>AutoScout24</title>
<!-- CSS of page -->
<link rel="stylesheet" href="/assets/home/ebacb8194-main.min.css" />
</head>
<body>
<!-- ESI include of header -->
<esi:include src="http://content.as24.com/fragment/header_de_DE" />
Lorem ipsum....
<!-- JavaScript of page -->
<script src="/assets/home/66ee72f9-main.min.js"></script>
</body>
</html>
16. ESI Include Resolved
16
<html>
<head>
<title>AutoScout24</title>
<!-- CSS of page -->
<link rel="stylesheet" href="/assets/home/ebacb8194-main.min.css" />
</head>
<body>
<!-- CSS of fragment -->
<link rel="stylesheet" href="http://content.as24.com/assets/08ffaf28-main.min.css" />
<ul><li>Home</li><li>Search</li><li>Sell</li></ul>
<!-- JavaScript of fragment -->
<script src="http://content.as24.com/assets/26ed612f-main.min.js"></script>
Lorem ipsum....
<!-- JavaScript of page -->
<script src="/assets/home/66ee72f9-main.min.js"></script>
</body>
</html>
17. Multiple ESI Includes
17
<html>
<head>
<title>AutoScout24</title>
<!-- CSS of page -->
<link rel="stylesheet" href="/assets/home/ebacb8194-main.min.css" />
<!-- ESI include for header CSS -->
<esi:include src="http://content.example.com/fragment/header_styles" />
</head>
<body>
<!-- ESI include for header -->
<esi:include src="http://content.example.com/fragment/header_de_DE" />
Lorem ipsum....
<!-- JavaScript for page -->
<script src="/assets/home/66ee72f9-main.min.js"></script>
<!-- ESI include for header JavaScript -->
<esi:include src="http://content.example.com/fragment/header_scripts" />
</body>
</html>
18. Multiple ESI Includes Resolved
18
<html>
<head>
<title>AutoScout24</title>
<!-- CSS for page -->
<link rel="stylesheet" href="/assets/home/ebacb8194-main.min.css" />
<!-- CSS for header -->
<link rel="stylesheet" href="http://content.example.com/assets/08ffaf28-main.css" />
</head>
<body>
<ul><li>Home</li><li>Search</li><li>Sell</li></ul>
Lorem ipsum....
<!-- JavaScript for page -->
<script src="/assets/home/66ee72f9-main.min.js"></script>
<!-- JavaScript for header -->
<script src="http://content.example.com/assets/26ed612f-main.js"></script>
</body>
</html>
19. Varnish & ESI
Composition Issues
19
• Bad page performance because of page structure
• Tries to optimize the page structure led to increased
complexity regarding the asset handling
• High burden on the content providing teams
20. Varnish & ESI
Additional Issues
20
• Combining assets with ESI adds lots of complexity
• Varnish cannot strong cache assets combined with ESI
• AWS ELB as Varnish backend wasn’t working (multiple
short-lived IPs)
21. Requirements for a better solution
21
• References to asset URIs need to be included in HTML
snippet
• Therefore post-processing of references is required
• Support for combined assets
• Support for inlining CSS/JS
• Support for shared cache between instances
29. Pages
29
are publicly accessible
get called from the client
include fragments
could be cacheable
define contracts
are parts of a page
get called from Nginx SSI
could include fragments
should be cacheable
adhere to contracts
Fragments
30. SSI Include
30
<html>
<head>
<title>AutoScout24</title>
<!-- Minified and combined css used by this page (not by the fragments) -->
<link rel="stylesheet" href="/assets/home/ebacb8194-main.min.css" />
</head>
<body>
<!--#include virtual="/headerservice/fragment/header_de_DE" -->
Lorem ipsum....
<!-- Minified and combined javascript used by this page -->
<script type="text/javascript" src="/assets/home/66ee72f9-main.min.js"></script>
</body>
</html>
31. SSI Include Resolved
31
<html>
<head>
<title>AutoScout24</title>
<!-- Minified and combined css used by this page (not by the fragments) -->
<link rel="stylesheet" href="/assets/home/ebacb8194-main.min.css" />
</head>
<body>
<head>
<!-- Minified and combined css used by this fragment -->
<link rel="stylesheet" href="/assets/headerservice/08ffaf28-main.min.css" />
</head>
<ul><li>Home</li><li>Search</li><li>Sell</li></ul>
<script type="text/javascript" src="/assets/headerservice/26ed612f-main.js"></script>
Lorem ipsum....
<!-- Minified and combined javascript used by this page -->
<script type="text/javascript" src="/assets/home/66ee72f9-main.min.js"></script>
</body>
</html>
32. ngx_pagespeed: combine heads
32
<html>
<head>
<title>AutoScout24</title>
<!-- Minified and combined css used by this page (not by the fragments) -->
<link rel="stylesheet" href="/assets/home/ebacb8194-main.min.css" />
<link rel="stylesheet" href="/assets/headerservice/08ffaf28-main.min.css" />
</head>
<body>
<ul><li>Home</li><li>Search</li><li>Sell</li></ul>
<script type="text/javascript" src="/assets/headerservice/26ed612f-main.js"></script>
Lorem ipsum....
<!-- Minified and combined javascript used by this page -->
<script type="text/javascript" src="/assets/home/66ee72f9-main.min.js"></script>
</body>
</html>
38. Pagespeed Cache
38
• Caches generated assets
• memcached (ElastiCache on AWS)
• state is externalized to AWS
• allows for stateless web server machines
• no cache synchronization
• no cold cache
39. nginx Proxy Cache
39
• Caches responses from upstream services
• Respects cache headers from upstream services
• Supports cache key control via Vary Header
• AWS ElastiCache (via ngx_srcache module)
48. Jigsaw Best Practice Analyzer
48
• Checks HTML code for anti-patterns
• defer async
• page barriers (inline scripts)
• CSS outside of <head>
• stylesheet refs with different attributes
• Assets not located in /assets/
• Can run in a deployment pipeline
49. Things yet to be solved
49
• Authentication is not in scope yet
• A/B testing of fragments
• JavaScript integration / interaction
• Bootstrap fragment / common things
• Native mobile apps
51. Features of the UI Composition Solution
51
• Teams are in full control of their service's UI and do
not need rely on others when changing it
• Fragments have a simple structure with head, body
and script parts
• Page performance is not compromised
• Jigsaw serves as an effective cache layer
• Fragments can be tested in isolation, and in
integration with other pages or fragments
52. Learnings and Practices
52
• Try to keep composition layer as simple as possible!
• Stick to HTTP protocol use cases
• Allow services to control Jigsaw's caching behavior
• Isolate fragments by CSS and JS packages
• Try hard to have good documentation
> Arif
Before we go into the topic of UI composition, I’d like to give you some background about our motivation and the context in which we developed our solution to UI composition.
And that context is a project that we call Tatsu.
Tatsu means dragon (logo), because the project is nothing less than a complete rewrite of the AS24 platform, which is live in 18 countries across Europe, and therefore we compare the project with a dangerous but powerful dragon.
In this project, which started a bit more than a year ago, we are doing 5 things at the same time.
We are breaking the old rather monolithic system into a set of independent microservices.
We are moving from a self hosted system to a completely cloud hosted system, using Amazon Web Services.
We are moving from a Microsoft .NET stack to a Linux- and JVM-based tech stack.
Because that is not enough, we are also changing the organisational structure, so that it reflects the Microservice approach.
That means, we move from multiple product engineering teams that rely on a few operations and platform teams to cross-functional, autonomous teams that have enough ops knowledge to run their own services. This is depicted by those gradients here.
Finally, as we are rebuilding everything, we want to make sure that we come out of the whole process with a new shiny AS24 page that contains only those features that are actually useful.
Now, this whole project is obviously a huge effort, so why are we doing it?
One of the reasons is to attract talented developers, which was harder with the old .NET tech stack
However, the main reason is to reduce time to market, so that we can respond to user needs more quickly
For this it is important, that new features can be released very quickly, also for being able to do lots of A/B testing to get early user feedback
The idea here is to be able to think in minutes for feature to be released instead of thinking in days
Now for this to be possible, teams must be enabled to innovate independently
We believe that a key to achieve this are autonomous teams, that are able to independently ...
Therefore, we believe that it is crucial to avoid any form of tight coupling as much as possible
If a team wants to improve their own service but must wait for another team to release something in order to be able to do so then there is no real autonomy.
In fact, we value this team autonomy so much, that we also accept certain trade-offs
E.g., we often rather copy some code around than to create tight coupling by introducing a shared library or shared infrastructure
At the same time, we do not want to compromise page performance.
In case you do not know it, Pagespeed Insights is a tool by Google that analyzes the structure of a page in terms of how fast it can be rendered, in particular on a mobile browser.
...and the score goes up to 100, and is considered good from 85 upwards.
This means, that we heavily rely on different cache layers and that our pages must not contain blocking content.
In the context of Microservices, it is a bit tricky to achieve both high page speed and high team autonomy.
To explain this, I am now going to present you two different ways how to generally approach this problem.
And then afterwards Johannes will go more into detail on how we actually tried to solve it.
The first one is the API Gateway pattern
In this example you see that there are already several backend services such as...
So the monolith is already broken down into several independent services at the back end
However, in the frontend, there is still one single web application that takes care of the whole web UI, and talks to the different backend APIs.
E.g., this red UI component here displays some content that is provided by the homepage service, whereas this
In order to avoid that the web app needs to start several separate requests to several backend services just to perform a single action, it is generally a good idea to introduce a gateway layer that aggregates those requests to backend services.
This way, the number of requests is reduced which is particularly important when using a mobile device to view the page.
The API Gateway pattern is also particularly useful to deal with native mobile apps, however, those won't be in the focus of this talk today.
Now, of course, in this scenario it is is also fairly easy to optimize the performance of the page because all the UI components belong to one application.
However, there are some drawbacks of applying this pattern from which, for us, the most severe is that independent feature releases are no longer possible (as long as they contain UI changes)
Instead, in the worst case three teams are involved and need to be coordinated when changing a feature: the backend service team, the API gateway layer team, and the web app team
One could say that the API Gateway pattern one goes only half way to split the monolith, i.e., only in the backend
There are other drawbacks such as...
but because team autonomy is one of the main reasons we went for Microservices in the first place, this issue made the API Gateway pattern no option for us
Instead we went for another pattern and that is the UI composition pattern
This looks as follows: Each service delivers its own UI component, e.g., red home page
Those UI components are later composed to a single web page
This way, each team can change their backend and their UI independently
We also often speak of vertical slices here, because there is no layered architecture anymore and each service can contain a backend, a UI, and everything in between
However, there are quite few challenges that come with this approach
... each part of the page could compromise page performance by introducing render blocking content
... the loading time of the page depends on its slowest component
Jo
why are we telling you this -> to narrow down the topic to the core questions to solve
Varnish test setup infrastructure
Varnish in front of services
no service registration, just loading content from referenced URLs
Page, therefore HTML tag
ESI include syntax
styles and scripts already minified by service
ESI resolved = include replaced with fragment (by Varnish)
fragment has its own minified styles and scripts in there
also its own script section at its bottom
need to have full qualified references to assets
very bad page speed
One ESI include for header styles
One ESI include for header fragment
One ESI include for header scripts
Multiple ESI resolved = include replaced with fragment (by Varnish)
One ESI include for header styles
One ESI include for header fragment
One ESI include for header scripts
better page speed, but still not perfect (no combination etc)
big problem: TTL of cached includes are not synchronized. Version mismatches!
> Arif
So before showing an example how this actually looks in code
I have to explain that we conceptually distinguish between what we call a page and what we call a fragment
...and does not know the original request
...and this team is responsible for the performance and the stability of the page
So when this page includes fragments, this team has to define a contract with the providers of this fragment
The fragment on the other hand needs to obey this contract and behave well
Now let's look at a simple example page
Page, therefore HTML tag+ head and body
styles already minified by service
SSI include syntax
SSI resolved = include replaced with fragment (by Nginx)
fragment has its own head, with its own minified styles in there
also its own script section at its bottom
very bad page speed as of now
now comes mod pagespeed
combine heads
combine css
combine js, defer its loading
> Jo
Example: personalized pages (different content based on visitor, makes no sense to cache it)
> Jo
3 parts on home page
let's assume you want to change header and test
defer async stops pagespeed from combining
page barrier = inline script, order important, cannot combine assets above and below barrier
stylesheet --> for example media attribute
authentication and a/b testing -> need to get cookies and headers to the fragments
javascript integration / interaction -> window.event or something else??
Bootstrap: for example Fonts or javascript error logger etc.
> Arif
...as long as they stay within the boundaries of their contracts