Knots - the Lazy Data Transfer Objects for Dealing with the Microservices Craze

Preview:

Citation preview

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: 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”

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