Microservices architectures are distributed ones, thus using them has perils. 'Knots' are a trivial, almost mechanical way to represent the flow of data in them that is composable, extendable and relatively easy to reason about. It simplifies refactors and maintenance and makes roundtrips easy to account. They are compatible with caching, different architectures. Just get an ID and promise to give what it corresponds to when you ask for it.
Microservices make database joins either redundant application level job or very difficult application level job depending on whether you are designing the microservice or are using it. Even though joins may be impossible you need to tie data together - that's why you use knots. A knot may tie several things together, it may even tie one knot to another.
In design patterns parlance knot is just your run of the mill lazily instantiated facade proxy for data transfer that you can observe when you have to. Except it is not! It is a knot.
Just have an id and you will get it.
And once you get it - keep it knotted together.
Apidays New York 2024 - The value of a flexible API Management solution for O...
Knots - the Lazy Data Transfer Objects for Dealing with the Microservices Craze
1. Knots – the Lazy Data Transfer
Objects for Dealing with the
Microservices Craze
2. [ashopov@ashmac ~]$ whoami
By day: Software Engineer at Uber
By night: OSS contributor
Coordinator of Bulgarian Gnome TP
Git, bash, Sentry, Jenkins speak Bulgarian
Contacts:
E-mail: ash@kambanaria.org
LinkedIn: http://www.linkedin.com/in/alshopov
SlideShare: http://www.slideshare.net/al_shopov
GitHub: https://github.com/alshopov
Web: Just search “al_shopov”
3. Please Learn and Share
License: Creative Commons
Attribution 4.0 International
(CC-BY v4.0)
4. The Whole Lecture in One Slide
// KNOT
public class UserKnot {
private final int userId;
private User user;
public UserKnot(int userId) {
this.userId = userId;
}
public int getUserId() {
return userId;
}
public User getUser() {
if (user == null) {
user = USER_SERVICE.
getUserById(userId);
}
return user;
}
}
// BEAN-ish, no no-args constr.
public class UserBean {
private int userId;
private User user;
public UserBean(int userId,
User user) {
this.userId = userId;
this.user = user;
}
public int getUserId() {
return userId;
}
public User getUser() {
return user;
}
}
|
|
5. What Did Microservices Give Us?
● Many services:
– Bigger than micro (geodes)
– Lesser than micro (nanoservices)
● Every solution is another micro service:
– Did we have a problem?
– Was it the right problem?
6. Fundamental Theorem of Software
Engineering
All problems in computer
science can be solved by
another level of indirection,
except of course for the
problem of too many
indirections
7. Fundamental Theorem of Software
Practice
All problems in a
microservices architecture
can be solved by other
microsrvices, except of
course for the problem of too
many microservices.
9. Dude, Where Are My JOINs?
● Data relates to other data.
● You may denormalize but you cannot have all
microservices have all the data
● Data is isolated in domains, different microservices
serve it and you have to re-join it
● Single source of truth? What do you mean by truth?
● A whole workflow is like a quest – Raiders of the Lost
Join – you go to different services, ask questions and
get answers
– What order?
– How many times?
10. micro SERVICES
● No matter how big they are, they are services
● They are at least a network call away
● Money cannot buy time!
● Money can buy memory, servers, disks, more
bandwidth, engineers
● 299 792 458 m / s – it is the law. Even in
Pernik!
11. DRY, KISS, YAGNI for Microservices
● Do not repeat your queries for the same data if
you can avoid it
– Once you get the data – keep it
● Keep this avoiding simple
– There are many services, you cannot pass
the data of all of them as arguments in all
combinations
● If you do not need some data – you ain’t gonna
need it
– Load as lazily as you can
12. Keep on Adding, Pass it All Around
● Through layers ● Through modules
13. This Is the Essence of Tying a Knot
// KNOT
public class UserKnot {
private final int userId;
private User user;
public UserKnot(int userId) {
this.userId = userId;
}
public int getUserId() {
return userId;
}
public User getUser() {
if (user == null) {
user = Registry.
getInstance().
getUserService().
getUserById(userId);
}
return user;
}
}
DRY – the second call to getUser
does not repeat the request
KISS – getUser hides specifics
service are behind a getter
YAGNI – if you never call getUser –
you will not incur a network call
14. This Is the Essence of Tying a Knot
// KNOT
public class UserKnot {
private final int userId;
private User user;
public UserKnot(int userId) {
this.userId = userId;
}
public int getUserId() {
return userId;
}
public User getUser() {
if (user == null) {
user = Registry.
getInstance().
getUserService().
getUserById(userId);
}
return user;
}
}
DRY – the second call to getUser
does not repeat the request
KISS – getUser hides specifics
service are behind a getter
YAGNI – if you never call getUser –
you will not incur a network call
SELECT *
FROM users AS u
WHERE u.id=42;
15. Extendable – Direct Joins
public class ExtendedUserKnot {
private final int userId;
private User user;
private List<Account> accounts;
public ExtendedUserKnot(int userId) {
this.userId = userId;
}
public int getUserId() {
return userId;
}
public User getUser() {
if (user == null) {
user = USER_SERVICE.getUserById(userId);
}
return user;
}
public List<Account> getAccounts() {
if (accounts == null) {
accounts = ACCOUNT_SERVICE.getAccountByUserId(userId);
}
return accounts;
}
}
Join another service
16. Extendable – Direct Joins
public class ExtendedUserKnot {
private final int userId;
private User user;
private List<Account> accounts;
public ExtendedUserKnot(int userId) {
this.userId = userId;
}
public int getUserId() {
return userId;
}
public User getUser() {
if (user == null) {
user = USER_SERVICE.getUserById(userId);
}
return user;
}
public List<Account> getAccounts() {
if (accounts == null) {
accounts = ACCOUNT_SERVICE.getAccountByUserId(userId);
}
return accounts;
}
}
Join another service
SELECT *
FROM users AS u
JOIN accounts AS a
ON u.id=a.user_id
WHERE u.id=42;
17. Extendable – Multiple Joins
public class DoubleUserKnot {
private final int userId;userId;
private final int bankId;
private User user;
private Bank bank;
private accounts;
public DoubleUserKnot(int userId, int bankId) {
this.userId = userId;
this.bankId = bankId;
}
public int getUserId() {
return userId;
}
public User getUser() {
if (user == null) {
user = USER_SERVICE.getUserById(userId);
}
return user;
}
public List<Account> getAccounts() {
if (accounts == null) {
accounts = ACCOUNT_SERVICE.
getAccountByUserIdBankId(userId, bankId);
}
return accounts;
}
public Bank getBank() {
if (bank == null) {
bank = BANK_SERVICE.getBankById(bankId);
}
return bank;
}
}
Capture several attributes
Join many services
Service depends on several attributes
18. Extendable – Multiple Joins
public class DoubleUserKnot {
private final int userId;userId;
private final int bankId;
private User user;
private Bank bank;
private accounts;
public DoubleUserKnot(int userId, int bankId) {
this.userId = userId;
this.bankId = bankId;
}
public int getUserId() {
return userId;
}
public User getUser() {
if (user == null) {
user = USER_SERVICE.getUserById(userId);
}
return user;
}
public List<Account> getAccounts() {
if (accounts == null) {
accounts = ACCOUNT_SERVICE.
getAccountByUserIdBankId(userId, bankId);
}
return accounts;
}
public Bank getBank() {
if (bank == null) {
bank = BANK_SERVICE.getBankById(bankId);
}
return bank;
}
}
Capture several attributes
Join many services
Service depends on several attributes
SELECT *
FROM users AS u
JOIN accounts AS a
ON u.id=a.user_id
JOIN banks AS b
ON u.id=a.bank_id
WHERE u.id=42
AND b.id=666;
19. Composable – Knot Within a Knot
public class CountryKnot {
private final int id;
private Country country;
private Currency currency;
public CountryKnot(int id) {
this.id = id;
}
public int getId() {
return id;
}
public Country getCountry() {
if (country == null) {
country = COUNTRY_SERVICE.
getCountryById(id);
}
return country;
}
public Currency getCurrency() {
if (currency == null) {
currency = CURRENCY_SERVICE.
GetCurrencyById(
getCountry().
getCurrencyId());
}
return currency;
}
}
public class ComposedUserKnot {
private final int userId;
private User user;
private CountryKnot countryKnot;
public ComposedUserKnot(int userId) {
this.userId = userId;
}
public User getUser(){
if (user == null){
user = USER_SERVICE.
getUserById(userId);
}
return user;
}
public Country getCountry(){
if (countryKnot == null){
countryKnot = new CountryKnot(
getUser().
getCountryId());
}
return countryKnot.getCountry();
}
public Currency getCurrency() {
return countryKnot.getCurrency();
}
}
20. Composable – Knot Within a Knot
public class CountryKnot {
private final int id;
private Country country;
private Currency currency;
public CountryKnot(int id) {
this.id = id;
}
public int getId() {
return id;
}
public Country getCountry() {
if (country == null) {
country = COUNTRY_SERVICE.
getCountryById(id);
}
return country;
}
public Currency getCurrency() {
if (currency == null) {
currency = CURRENCY_SERVICE.
GetCurrencyById(
getCountry().
getCurrencyId());
}
return currency;
}
}
public class ComposedUserKnot {
private final int userId;
private User user;
private CountryKnot countryKnot;
public ComposedUserKnot(int userId) {
this.userId = userId;
}
public User getUser(){
if (user == null){
user = USER_SERVICE.
getUserById(userId);
}
return user;
}
public Country getCountry(){
if (countryKnot == null){
countryKnot = new CountryKnot(
getUser().
getCountryId());
}
return countryKnot.getCountry();
}
public Currency getCurrency() {
return countryKnot.getCurrency();
}
}
SELECT *
FROM users AS u
JOIN countries AS c
ON u.country_id=c.id
JOIN currency AS cu
ON c.currency_id=cu.id
WHERE u.id=42;
21. Threads, Anybody?
● A lot of frameworks get a request–response cycle
in a single thread, all the layers are in the thread
that got the initial data, so no synchronization is
needed
● If your knots will be touched by many threads –
you need some synchronization.
● First to request – will block until knot is tied.
● The rest of requesters – will also have to wait.
– It is rare that you need to issue the same
request several times – unreliable network,
changing routing, etc.
22. Thread safety
Double checked
locking
public class ThreadSafeUserKnot {
// User MUST be immutable, reference MUST be volatile
// userId MUST be final
// Every service MUST have own lock
private final int userId;
private volatile User user;
private final Object userLock = new Object();
public ThreadSafeUserKnot(int userId) {
this.userId = userId;
}
public int getUserId() {
return userId;
}
public User getUser() {
if (user == null) {
synchronized (userLock) {
if (user == null) {
user = USER_SERVICE.getUserById(userId);
}}}
return user;
}
}
23. Lazily Instantiated
● The magic behind knots is that we query external
services lazily – not earlier than needed.
● We do not incur network latency if we do not need
the network call.
● While the full workflow may need many calls,
parts may take decision based on partial
information – quick bailout.
● It is easier going from lazy to eager fetches – but
more about this when we talk about observables.
24. Facade
● The knots serve as a facade to external
services
● You capture request info in constructor
● All peculiarities of the different network services
are hidden behind a simple getter.
● Easier to use, easier to read
25. Proxy
● Network calls are expensive, so we proxy
● Proxying and forwarding allows us to cache the
result per request
● Proxying works because we mainly read, thus
we do not need full functioning objects that we
can modify
26. Data Transfer Object
● It holds all data
● But no need for serialization – a knot is always
local. Crossing microservices frequently means
crossing language and framework barriers.
● Similarly – no business logic, but consistency
checking and validation are important since
data is shared across many services. There is
duplication and a knot may check consistency
of data.
27. Compared to ORM Entities
● Knot is readonly
● Synchronizes once per request
● Explicitly shows slow calls
● Always starts lazy and predominantly stays so
● When knots stop being so lazy they become
parallel
● SELECT n+1 – for entities bad performance, for
knots – impossible performance
– Entities – joins or lazy
– Knots – already lazy, no joins – bulk APIs
28. What About Testing?
● Testing is very easy if you have mocked the
services. Knots basically aggregate the objects
returned by the mocks
● No business logic – nothing to test
● Verification of data from different services
– Very important
– Couples with observability
29. Refactoring
● If you have too many services calls per request –
make whoever calls you provide you with some of
the information you need, you push more in the
constructor, you have shorter chain of knots
● If you cannot trust the input and need to get it on
your own – you go the other direction
● Knots make both possible and compatible
● The rest of refactoring is your microservice. Knots
separate you from changes in other
microservices.
30. Observable
● Most often you log network calls and then
reconstruct calls, usually you have a request
identifier
● Knots allow you to reverse and/or augment this
logging
● Do you always call service B after calling
service A?
– Issue both calls together
● If you call service T to get some data but it is
also available elsewhere – stop calling T.
31.
32. Knots’ Single Responsibility
● On the one hand they break it because they
knot together many services
● On the other – their primary purpose is to
minimize network calls
● Make using microservices easier
● Transparent yet not abstract
33.
34. Open/Closed
● Knots are closed for modification and not
extendable
● You can implement an open basic class and do
tricks with generics, however this implies very
similar microservices which is not true in
practice
● There is only so much you can get from
extension, sorry
● Knots should be easy to understand and trace,
do not be overly creative with them
35.
36. Law of Demeter
● Talk to immediate friends, not strangers
● Method A.a() calls method B.b() and not
B.c.d.f()
● Tell, don’t ask
● Break it regarding to knots, you may reach
inside them – they can be arbitrarily nested
even though microservices have flatter
structure at least initially
● This allows to organize the rest of the codebase
to upkeep the law
37. Compare Similar Solutions
● Spring’s Request Scoped Beans
– Only Java
– Only Spring
– Web container targeted
● VMWare’s Xenon
– Again Java only
– Much larger scope – whole framework
– Much tighter integration, your code functions
inside it
38. That’s All, Folks
● Questions?
● Anything to declare?
● Microservices? To Knot or Not?