knots - the lazy data transfer objects for dealing with the microservices craze
TRANSCRIPT
Knots – the Lazy Data Transfer Objects for Dealing with the
Microservices Craze
[ashopov@ashmac ~]$ whoami
By day: Software Engineer at UberBy night: OSS contributorCoordinator of Bulgarian Gnome TPGit, bash, Sentry, Jenkins speak Bulgarian
Contacts: E-mail: [email protected] LinkedIn: http://www.linkedin.com/in/alshopov SlideShare: http://www.slideshare.net/al_shopov GitHub: https://github.com/alshopov Web: Just search “al_shopov”
Please Learn and Share
License: Creative Commons Attribution 4.0 International
(CC-BY v4.0)
The Whole Lecture in One Slide
// KNOTpublic 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; }}||
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?
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
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.
What Did Microservices Take Away From Us?
?
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?
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!
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
Keep on Adding, Pass it All Around
● Through layers ● Through modules
This Is the Essence of Tying a Knot
// KNOTpublic 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 getUserdoes not repeat the request
KISS – getUser hides specificsservice are behind a getter
YAGNI – if you never call getUser –you will not incur a network call
This Is the Essence of Tying a Knot
// KNOTpublic 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 getUserdoes not repeat the request
KISS – getUser hides specificsservice 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;
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
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;
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
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;
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(); }}
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;
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.
Thread safetyDouble 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; }}
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.
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
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
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.
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
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
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.
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.
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
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
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
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
That’s All, Folks
● Questions?● Anything to declare?● Microservices? To Knot or Not?
● Images: Wikimedia Commons