@doanduyhai
KillrChat, the scalable chat with
AngularJS, Cassandra & Spring Boot
DuyHai DOAN, Technical Advocate
@doanduyhai http://goo.gl/gKxIHb
Who am I ?!
2
Duy Hai DOAN
Cassandra technical advocate
•  talks, meetups, confs
•  open-source devs (Achilles, KillrChat …)
•  OSS Cassandra point of contact
☞ duy_hai.doan@datastax.com
•  Datastax = OSS Cassandra + extra features
@doanduyhai http://goo.gl/gKxIHb
Why KillrChat ?!
3
•  Cassandra Hands-on exercise
•  Real working app, not toy example
•  Scalable design
•  FUN
•  AngularJS, Spring Boot
@doanduyhai http://goo.gl/gKxIHb
Architecture!
Single node
4
Tomcat
Spring
C*
HTTP Rest
AngularJS,
UI Bootstrap,
SockJS
Spring {Boot,Security,
Rest, Messaging}
Cassandra,
Achilles
@doanduyhai http://goo.gl/gKxIHb
Architecture!
5
Tomcat
Spring
C*
HTTP Rest
Spring {Boot,Security,
Rest, Messaging}
Cassandra,
Achilles
In-memory broker
Embedded/local
Cassandra
Single node
AngularJS,
UI Bootstrap,
SockJS
@doanduyhai http://goo.gl/gKxIHb
Architecture!
6
Tomcat
Spring …
Broker (RabbitMQ, ZeroMQ, Kafka…)
Tomcat
Spring
Tomcat
Spring
Scaling out
@doanduyhai http://goo.gl/gKxIHb
Cassandra data distribution!
Random: hash of #partition → token = hash(#p)

Hash: ]-X, X]

X = huge number (264/2)

 n1
n2
n3
n4
n5
n6
n7
n8
7
@doanduyhai http://goo.gl/gKxIHb
Token Ranges!
A: ]0, X/8]
B: ] X/8, 2X/8]
C: ] 2X/8, 3X/8]
D: ] 3X/8, 4X/8]
E: ] 4X/8, 5X/8]
F: ] 5X/8, 6X/8]
G: ] 6X/8, 7X/8]
H: ] 7X/8, X]
n1
n2
n3
n4
n5
n6
n7
n8
A
B
C
D
E
F
G
H
8
@doanduyhai http://goo.gl/gKxIHb
How to scale ?!
With Cassandra
9
user_id1
user_id2
user_id3
user_id4
user_id5
n1
n2
n3
n4
n5
n6
n7
n8
A
B
C
D
E
F
G
H
@doanduyhai http://goo.gl/gKxIHb
How to scale ?!
With Cassandra ☞ distributed tables
10
n1
n2
n3
n4
n5
n6
n7
n8
A
B
C
D
E
F
G
H
user_id1
user_id2
user_id3
user_id4
user_id5
@doanduyhai http://goo.gl/gKxIHb
How to scale ?!
The broker
11
Broker (RabbitMQ, ZeroMQ, Kafka…)
Tomcat
Spring
@doanduyhai http://goo.gl/gKxIHb
Why AngularJS ?!
•  Easy
•  Extensible (directives)
•  Productive
•  Lot of resources/tutorial
•  Bootstrap integration (UI-bootstrap)
12
KillrChat
in action
@doanduyhai http://goo.gl/gKxIHb
The controllers!
14
Pass-through layer to services

Keep state of the view

Bind view behaviors to services methods
@doanduyhai http://goo.gl/gKxIHb
The controllers!
15
killrChat.controller('ListAllRoomsCtrl',6function($scope,6ListAllRoomsService){6
666666$scope.allRooms6=6[];6
666666$scope.joinRoom6=6function(roomToJoin)6{6
666666666ListAllRoomsService.joinRoom($scope,6roomToJoin);6
666666};6
666666$scope.quitRoom6=6function(roomToLeave)6{6
666666666ListAllRoomsService.quitRoom($scope,6roomToLeave);6
666666};6
666666$scope.$evalAsync(ListAllRoomsService.loadInitialRooms($scope));66
});6
@doanduyhai http://goo.gl/gKxIHb
The services!
16
Business logic

Stateless (scopes passed as param)

1 method = 1 responsibility

Lots of DI
@doanduyhai http://goo.gl/gKxIHb
The services!
17
killrChat.service('RememberMeService',function($rootScope,<$location,<$cookieStore,<RememberMe)<{...});<
<<<
killrChat.service('GeneralNotificationService',<function($rootScope)<{...});<
<
killrChat.service('SecurityService',<function($rootScope,<$location,<$cookieStore,<User)<{...});<<<
<
killrChat.service('UserRoomsService',<function(Room,<ParticipantService,<GeneralNotificationService)<{...});<<<
<
killrChat.service('ParticipantService',<function()<{...});<<<
<
killrChat.service('NavigationService',<function(Room,<ParticipantService,<UserRoomsService,<
GeneralNotificationService)<{...});<<<
<
killrChat.service('WebSocketService',<function(ParticipantService,<UserRoomsService,<GeneralNotificationService)<
{...});<<<
<
killrChat.service('ChatService',<function(Message,<WebSocketService,<GeneralNotificationService)<{...});<<<
<
killrChat.service('RoomService',<function(ParticipantService)<{...});<<<
<
killrChat.service('ListAllRoomsService',<function(Room,<RoomService,<ParticipantService,<
GeneralNotificationService)<{...});<<<
<
killrChat.service('RoomCreationService',<function(Room)<{...});<
@doanduyhai http://goo.gl/gKxIHb
The services!
18
killrChat.service('ParticipantService',5function(){5
55555var5self5=5this;5
55555this.sortParticipant5=5function(participantA,participantB){5
555555555return5participantA.firstname.localeCompare(participantB.firstname);5
55555};5
55555this.addParticipantToCurrentRoom5=5function(currentRoom,5participantToAdd)5{5
555555555currentRoom.participants.push(participantToAdd);5
555555555currentRoom.participants.sort(self.sortParticipant);5
55555};5
55555this.removeParticipantFromCurrentRoom5=5function(currentRoom,5participantToRemove)5{5
555555555var5indexToRemove5=5currentRoom5
55 5 .participants5
55 5 .map(function(p)5{return5p.login})5
55 5 .indexOf(participantToRemove.login);5
555555555currentRoom.participants.splice(indexToRemove,51);5
55555};5
});5
@doanduyhai http://goo.gl/gKxIHb
The REST resources!
19
Leverage Angular $resource

Lots of custom behaviors

1 root path, many actions

Tricks & traps with $promise
@doanduyhai http://goo.gl/gKxIHb
The REST resources!
20
killrChat.factory('User',4function($resource)4{4
44444return4$resource('users',4[],{4
444444444'create':4{url:4'users',4method:4'POST',4isArray:false,4…},4
444444444'login':4{url:4'authenticate',4method:4'POST',4isArray:false,4…},4
444444444'load':4{url4:4'users/:login',4method:'GET',4isArray:false,4…},4
444444444'logout':4{url:4'logout',4method:4'GET',4isArray:false,4…});4
4});4
killrChat.factory('Room',3function($resource)3{3
33333return3$resource('rooms',3[],{3
333333333'create':3{url:3'rooms/:roomName',3method:3'POST',3isArray:false,...},3
333333333'delete':3{url:3'rooms/:roomName',3method:3'PATCH',3isArray:false,...},3
333333333"addParticipant":3{url:3'rooms/participant/:roomName',3method:3'PUT',...},3
333333333'removeParticipant':3{url:3'rooms/participant/:roomName',3method:3'PATCH',...},3
333333333'load':3{url:3'rooms/:roomName',3method:3'GET',3isArray:false,...},3
333333333'list':3{url:3'rooms',3method:3'GET',3isArray:true,...}3
33333});3
});3
@doanduyhai http://goo.gl/gKxIHb
$resource tricks & traps!
21
https://docs.angularjs.org/api/ngResource/service/$resource
• HTTP GET "class" actions: Resource.action([parameters], [success], [error])
• non-GET "class" actions: Resource.action([parameters], postData, [success], [error])
• non-GET instance actions: instance.$action([parameters], [success], [error])
@doanduyhai http://goo.gl/gKxIHb
$resource tricks & traps!
22
vs
• HTTP GET "class" actions: Resource.action([parameters], [success], [error])
• non-GET "class" actions: Resource.action([parameters], postData, [success], [error])
• non-GET instance actions: instance.$action([parameters], [success], [error])
ctions: Resource.action([parameters], [success], [error
tions: Resource.action([parameters], postData, [success
ctions: instance.$action([parameters], [success], [erro
• HTTP GET "class" actions: Resource.action([paramete
• non-GET "class" actions: Resource.action([parameter
• non-GET instance actions: instance.$action([paramet
https://docs.angularjs.org/api/ngResource/service/$resource
@doanduyhai http://goo.gl/gKxIHb
$resource tricks & traps!
23
What’s about $promise ?
@doanduyhai http://goo.gl/gKxIHb
$resource tricks & traps!
24
What’s about $promise ?
@doanduyhai http://goo.gl/gKxIHb
$resource tricks & traps!
25
What’s about $promise ?
Room.load({roomName:roomToEnter})4
4444444444444.$promise4
4444444444444.then(function(...){...})4
4444444444444.catch(function(...){...});4
new$User().$login({$
$$$$$$$$$$$$$j_username:$$scope.username,$
$$$$$$$$$$$$$j_password:$$scope.password,$
$$$$$$$$$$$$$_spring_security_remember_me:$$scope.rememberMe$
$$$$$$$$$})$
$$$$$$$$$.then(function()${...})$
$$$$$$$$$.catch(function(...){...});$
{
$promise: ….,
…
}
{
then: function(….),
catch: function(…),
…
}
@doanduyhai http://goo.gl/gKxIHb
The directives!
26
Good understanding of Angular internals

https://github.com/Zenika/angular-from-scratch

Javascript mastering is required

Basic knowledge of JQuery is a +
@doanduyhai http://goo.gl/gKxIHb
Password match directive!
27
@doanduyhai http://goo.gl/gKxIHb
Password match directive!
28
<input'type="password"'class="form6control"'name="password"'
''''''placeholder="Password"'
''''''ng6model="user.password"'required>'''
'
<input'type="password"'class="form6control"'name="confirm_password"'
''''''placeholder="Password'Confirm"'
''''''password6match="user.password"'
''''''ng6model="user.passwordConfirm"'required>!
@doanduyhai http://goo.gl/gKxIHb
Password match directive!
29
killrChat.directive('passwordMatch',6function()6{6
66666return6{6
666666666require:6'ngModel',6
666666666...,6
666666666link:6function6(scope,6el,6attrs,6ngCtrl)6{6
6666666666666scope.$watch(function(){6
66666666666666666//6check6for6equality6in6the6watcher6and6return6validity6flag6
66666666666666666var6modelValue6=6ngCtrl.$modelValue;6
66666666666666666return6(ngCtrl.$pristine6&&6angular.isUndefined(modelValue))6||66
6 6 6 angular.equals(modelValue,6scope.passwordMatch);6
6666666666666},function(validBoolean){6
66666666666666666//6set6validation6with6validity6in6the6listener6
66666666666666666ngCtrl.$setValidity('passwordMatch',6validBoolean);6
6666666666666});6
666666666}6
66666}6
6}666
@doanduyhai http://goo.gl/gKxIHb
Chat zone directive!
30
@doanduyhai http://goo.gl/gKxIHb
Chat zone directive!
31
<!##Chat(Main(Section##>(
<chat#zone((
( state="state"(//Reference(to(parent(scope(state(object(
( user="user"(//Reference(to($rootScope.user(
( home="home()"((//Reference(to(home()(method(
( get#light#model="getLightModel()"//Reference(to(getLightModel()(method(
>(
</chat#zone>(
@doanduyhai http://goo.gl/gKxIHb
Chat zone directive!
3 scroll modes
•  display (scroll down on new message)
•  fixed (de-activate automatic scroll)
•  loading (scroll up when old messages loaded)

32
@doanduyhai http://goo.gl/gKxIHb
Chat zone directive!
Display
33
messageszone
displayzone
autoscroll-down
@doanduyhai http://goo.gl/gKxIHb
Chat zone directive!
Fixed
34
fixedposition
@doanduyhai http://goo.gl/gKxIHb
Chat zone directive!
Loading
35
autoscroll-up
@doanduyhai http://goo.gl/gKxIHb
Chat zone directive!
36
killrChat.directive('chatZone',4function(...)4{4
44444return4{4
444444444...,4
4 link:4function4(scope,4root)4{4
4444444444444...4
4444444444444//Change4in4the4list4of4chat4messages4should4be4intercepted4
4444444444444scope.$watchCollection(4
44444444444444444function()4{44//4watch4on4chat4message4
444444444444444444444return4scope.messages;4
44444444444444444},4
44444444444444444function(newMessages,oldMessages)4{4//4on4change4of4chat4messages4
444444444444444444444if(scrollMode4===4'display')4{4
4444444444444444444444444if(newMessages.length4>4oldMessages.length){4
44444444444444444444444444444//4scroll4down4on4new4chat4messages4
4444444444444444444444444}4else4if(scrollMode4===4'loading')4{4
4 4444444444444444444444444element.scrollTop4=410;4
4 44444444444444444}4
44444444444444444}4
4444444444444);4
@doanduyhai http://goo.gl/gKxIHb
Chat zone directive!
37
!!!!!!!!!!!!!wrappedElement.bind('scroll',!function()!{!
!!!!!!!!!!!!!!!!!if(!<!<<!user!scrolls!down!<<>!)!{!
!!!!!!!!!!!!!!!!!!!!!scrollMode!=!'display';!
!!!!!!!!!!!!!!!!!}!else!if(!<!<<!user!scroll!up!&&!more!data!to!load!<<>!)!{!
!!!!!!!!!!!!!!!!!!!!!scope.$apply(function(){!
!!!!!!!!!!!!!!!!!!!!!!!!!usSpinnerService.spin('loading<spinner');!
!!!!!!!!!!!!!!!!!!!!!!!!!scrollMode!=!'loading';!
!!!!!!!!!!!!!!!!!!!!!!!!!//!load!previous!messages!
!!!!!!!!!!!!!!!!!!!!!});!
!!!!!!!!!!!!!!!!!!}!else!{!
!!!!!!!!!!!!!!!!!!!!!scrollMode!=!'fixed';!
!!!!!!!!!!!!!!!!!}!
!!!!!!!!!!!!!});!
@doanduyhai http://goo.gl/gKxIHb
WebSockets!
SockJS
•  use Stomp over plain TCP
•  publish/subscribe broker abstraction 
•  browsers subscribe to topics



38
@doanduyhai http://goo.gl/gKxIHb
WebSockets!
39
this.initSockets,=,function($scope),{,
, self.closeSocket($scope);,
, var,roomName,=,$scope.state.currentRoom.roomName;,
, $scope.socket.client,=,new,SockJS('/killrchat/chat');,
, var,stomp,=,Stomp.over($scope.socket.client);,
, stomp.connect({},,function(),{,
,,,,,,,,,,,,,stomp.subscribe('/topic/messages/'+roomName,,
,,,,,,,,,,,,,,,,,function(message){,self.notifyNewMessage($scope,message),});,
,,,,,,,,,,,,,stomp.subscribe('/topic/participants/'+roomName,,
,,,,,,,,,,,,,,,,,function(message),{,self.notifyParticipant($scope,,message),});,
,,,,,,,,,,,,,stomp.subscribe('/topic/action/'+roomName,,
,,,,,,,,,,,,,,,,,function(message),{,self.notifyRoomAction($scope,,message),});,
,,,,,,,,,});,
,,,,,,,,,$scope.socket.stomp,=,stomp;,
};,
Client-side
@doanduyhai http://goo.gl/gKxIHb
WebSockets!
40
Server-side


@Inject(private(SimpMessagingTemplate(template;(((
(
@RequestMapping(value(=("/{roomName}",(method(=(POST,(consumes(=(APPLICATION_JSON_VALUE)(
@ResponseStatus(HttpStatus.NO_CONTENT)((
public(void(postNewMessage(@PathVariable(String(roomName,(@NotNull(@RequestBody(@Valid(
MessagePosting(messagePosting)(throws(JsonProcessingException({(
(((((...(
(((((template.convertAndSend("/topic/messages/"+roomName,(messageModel);((
}(((
Q & R
! "!
Thank You
@doanduyhai
duy_hai.doan@datastax.com

Introduction to KillrChat