Scalable Application Architecture

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=47 Minko Gechev
December 09, 2016

Scalable Application Architecture

Modern web applications have constantly growing requirements and their complexity grows exponentially. Some of the biggest challenges in front of us are state management, testability and flexibility. Given the dynamic business environment we need to make sure our projects' design uses well known patterns most developers are familiar with. In this talk I’ll introduce a scalable Angular 2 application architecture, which answers the following requirements: testability, predictable state management, scalable communication layer and modular and robust design.

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=128

Minko Gechev

December 09, 2016
Tweet

Transcript

  1. Scalable Application rchitecture github.com/mgechev twitter.com/mgechev blog.mgechev.com Minko Gechev

  2. None
  3. None
  4. None
  5. Hold on for the second edition!

  6. Story Time

  7. Disclaimer • High-level architecture, doesn’t define interfaces • Suggestions, not

    rules • Works for me, may not work for you
  8. Design Architecture ⚡

  9. – Martin Fowler “…decisions that are hard to change…” Architecture

  10. High-level constraints of the project

  11. Dynamic requirements

  12. None
  13. Scalable & flexible data layer

  14. Data layer • RESTful API • WebSocket application service •

    WebRTC data-channel • Various data packages • Bert-RPC • JSON • etc.
  15. Scalable team & codebase

  16. None
  17. High-level decisions for the architecture

  18. Dynamic Requirements Scalable & flexible data layer Scalable team &

    codebase
  19. Dynamic Requirements Scalable & flexible data layer Scalable team &

    codebase
  20. abstraction |əbˈstrakʃ(ə)n| noun [ mass noun ] … 4 the

    process of considering something independently of its associations or attributes: the question cannot be considered in abstraction from the historical context in which it was raised.
  21. WebRTC Gateway WebSocket Gateway

  22. Gateway WebRTC Gateway WebSocket Gateway

  23. Dynamic Requirements Scalable & flexible data layer Scalable team &

    codebase
  24. Explicitness

  25. Explicitness • Explicit state mutation • Explicit package mapping •

    Others…
  26. Dynamic Requirements Scalable & flexible data layer Scalable team &

    codebase
  27. Consistent state management

  28. Consistent state • Domain objects should: • Have a single

    instance • Explicit update
  29. Single instance !=>

  30. Single instance !=> Identity map

  31. Single instance !=> Identity map Explicit update !=>

  32. Single instance !=> Identity map Explicit update !=> Strict conventions

  33. redux++

  34. redux++

  35. Sample Tech Stack • Angular 2 • RxJS • ngrx

    • TypeScript • ImmutableJS
  36. Sample application

  37. None
  38. None
  39. High-level architecture

  40. UI components Façade (Active Record-like) (provides simplified interface to the

    components) State management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  41. UI components Façade (Active Record-like) (provides simplified interface to the

    components) State management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  42. UI components Façade (Active Record-like) (provides simplified interface to the

    components) State management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  43. UI components Façade (Active Record-like) (provides simplified interface to the

    components) State management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  44. UI components Façade (Active Record-like) (provides simplified interface to the

    components) State management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  45. UI components

  46. UI components Façade (Active Record-like) (provides simplified interface to the

    components) State management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  47. game.component.ts @Component({ selector: 'sd-game', template: `...`, styles: [...] }) export

    class GameComponent { constructor(private _model: GameModel) {} changeHandler(data: string) { this._model.onProgress(data); } }
  48. game.component.ts @Component({ selector: 'sd-game', template: `...`, styles: [...] }) export

    class GameComponent { constructor(private _model: GameModel) {} changeHandler(data: string) { this._model.onProgress(data); } }
  49. game.component.ts @Component({ selector: 'sd-game', template: `...`, styles: [...] }) export

    class GameComponent { constructor(private _model: GameModel) {} changeHandler(data: string) { this._model.onProgress(data); } }
  50. UI components • Simple presentational logic • As stateless as

    possible • Delegate business logic to models
  51. UI components Façade (Active Record-like) (provides simplified interface to the

    components) State management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  52. class GameModel extends Model { game$: Observable<Game>; constructor(protected _store: Store<any>,

    @Inject(AsyncService) _services: AsyncService[]) { super(_services || []); this.game$ = this._store.select('game'); } completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this.performAsyncAction(action) .subscribe(() => this._store.dispatch(action)); } } game.model.ts
  53. game.model.ts class GameModel extends Model { game$: Observable<Game>; constructor(protected _store:

    Store<any>, @Inject(AsyncService) _services: AsyncService[]) { super(_services || []); this.game$ = this._store.select('game'); } completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this.performAsyncAction(action) .subscribe(() => this._store.dispatch(action)); } }
  54. class GameModel extends Model { game$: Observable<Game>; constructor(protected _store: Store<any>,

    @Inject(AsyncService) _services: AsyncService[]) { super(_services || []); this.game$ = this._store.select('game'); } completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this.performAsyncAction(action) .subscribe(() => this._store.dispatch(action)); } } game.model.ts
  55. BaseModel Model B Model A State tree Component tree state

    stream (game$) references (store.select(…))
  56. class GameModel extends Model { game$: Observable<Game>; constructor(protected _store: Store<any>,

    @Inject(AsyncService) _services: AsyncService[]) { super(_services || []); this.game$ = this._store.select('game'); } completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this.performAsyncAction(action) .subscribe(() => this._store.dispatch(action)); } } game.model.ts
  57. class GameModel extends Model { game$: Observable<Game>; constructor(protected _store: Store<any>,

    @Inject(AsyncService) _services: AsyncService[]) { super(_services || []); this.game$ = this._store.select('game'); } completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this.performAsyncAction(action) .subscribe(() => this._store.dispatch(action)); } } game.model.ts
  58. Component Model ngrx

  59. Component Model completeGame() ngrx

  60. Component Model dispatch(action) ngrx completeGame()

  61. Component Model ngrx stateObserver.next(state) dispatch(action) completeGame()

  62. Component Model stateObserver.next(state) dispatch(action) ngrx completeGame()

  63. class GameModel extends Model { game$: Observable<Game>; constructor(protected _store: Store<any>,

    @Inject(AsyncService) _services: AsyncService[]) { super(_services || []); this.game$ = this._store.select('game'); } completeGame(time: number, text: string) { const action = GameActions.completeGame(time, text); this.performAsyncAction(action) .subscribe(() => this._store.dispatch(action)); } } game.model.ts
  64. Models (Façade) • Map the state stream • Delegate business

    logic to reducers • Delegate data-access logic to async services
  65. Beware for violations of the SRP

  66. UI components Façade (Active Record-like) (provides simplified interface to the

    components) State management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  67. Component Model stateObserver.next(state) dispatch(action) ngrx onProgress()

  68. ngrx reducers dispatch(action) State tree Action New state New state

  69. game.reducer.ts export const gameReducer = (state: any = initialState.get('game'), action:

    Action) => { switch (action.type) { case START_GAME: state = fromJS({}); break; case GAME_PROGRESS: state = state.set('currentText', action.payload.text); break; } return state; };
  70. game.reducer.ts export const gameReducer = (state: any = initialState.get('game'), action:

    Action) => { switch (action.type) { case START_GAME: state = fromJS({}); break; case GAME_PROGRESS: state = state.set('currentText', action.payload.text); break; } return state; };
  71. game.reducer.ts export const gameReducer = (state: any = initialState.get('game'), action:

    Action) => { switch (action.type) { case START_GAME: state = fromJS({}); break; case GAME_PROGRESS: state = state.set('currentText', action.payload.text); break; } return state; };
  72. game.reducer.ts export const gameReducer = (state: any = initialState.get('game'), action:

    Action) => { switch (action.type) { case START_GAME: state = fromJS({}); break; case GAME_PROGRESS: state = state.set('currentText', action.payload.text); break; } return state; };
  73. game.component.ts … get currentText$() { return this._model.game$ .map((game: Game) =>

    game.currentText); } …
  74. game.component.ts … get currentText$() { return this._model.game$ .map((game: Game) =>

    game.currentText); } …
  75. game.component.ts … get currentText$() { return this._model.game$ .map((game: Game) =>

    game.currentText); } …
  76. game.component.html … <div> {{ currentText$ | async }} </div> …

  77. game.component.html … <div> {{ currentText$ | async }} </div> …

  78. Reducers • Pure functions of the state and an action

    • Implement the business logic
  79. Async Services

  80. UI components Façade (Active Record-like) (provides simplified interface to the

    components) State management Async services Gateways (HTTP, WS, WebRTC) Commands (RESTful, RPC) Payloads (BERT, JSON) Store Reducers
  81. Remote Service App

  82. Remote Service App

  83. Remote Service App

  84. export abstract class AsyncService { abstract process(data: Action): Observable<any>; }

    base.async-service.ts
  85. export class GameP2PService extends AsyncService { constructor(private _rtcGateway: WebRTCGateway, private

    _store: Store) { _rtcGateway.dataStream .map((data: any) => JSON.parse(data.toString())) .subscribe((command: any) => { command.method === PROGRESS && _store.dispatch(P2PActions.progress(command.payload.text)); }); } process(action: Action) { const commandBuilder = buildP2PCommand(action); if (commandBuilder) return commandBuilder().invoke(); } } game-p2p.async-service.ts
  86. export class GameP2PService extends AsyncService { constructor(private _rtcGateway: WebRTCGateway, private

    _store: Store) { _rtcGateway.dataStream .map((data: any) => JSON.parse(data.toString())) .subscribe((command: any) => { command.method === PROGRESS && _store.dispatch(P2PActions.progress(command.payload.text)); }); } process(action: Action) { const commandBuilder = buildP2PCommand(action); if (commandBuilder) return commandBuilder().invoke(); } } game-p2p.async-service.ts
  87. export class GameP2PService extends AsyncService { constructor(private _rtcGateway: WebRTCGateway, private

    _store: Store) { _rtcGateway.dataStream .map((data: any) => JSON.parse(data.toString())) .subscribe((command: any) => { command.method === PROGRESS && _store.dispatch(P2PActions.progress(command.payload.text)); }); } process(action: Action) { const commandBuilder = buildP2PCommand(action); if (commandBuilder) return commandBuilder().invoke(); } } game-p2p.async-service.ts
  88. Async services • Map actions to commands • Map responses/messages

    to actions • Use an abstract gateway
  89. None
  90. Recap

  91. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator signup(data) signup(data) RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name uses as user$
  92. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator signup(data) RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) uses as user$
  93. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data) uses as user$
  94. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data) uses as user$
  95. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data) uses as user$
  96. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data) uses as user$
  97. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data) uses as user$
  98. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data) uses as user$
  99. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data) signup(data) uses as user$
  100. Async services Business facade Business logic Communication logic Immutable app

    state Component tree root cmp sign-up form user UserModel User Action Creator signup(data) RESTful Async Service process(action) userReducer register() RESTful CommandBuilder build(action) Restful Command Restful Gateway invoke() send() creates() uses as user$ uses user$ Stream Dependency Action (manipulation/method call) User registration user.email = email user.name = name signup(data)
  101. None
  102. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Serializable state usable offline • Model can use different services based on context • Easy management of async events
  103. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Serializable state usable offline • Model can use different services based on context • Easy management of async events
  104. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Serializable state usable offline • Model can use different services based on context • Easy management of async events
  105. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Serializable state usable offline • Model can use different services based on context • Easy management of async events
  106. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Serializable state usable offline • Model can use different services based on context • Easy management of async events
  107. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Serializable state usable offline • Model can use different services based on context • Easy management of async events
  108. Properties… • Predictable state management • Testable (easy to mock

    services thanks to DI) • Not coupled to any remote service • Serializable state usable offline • Model can use different services based on context • Easy management of async events
  109. Resources • redux • ngrx • mgv.io/scalable-architecture • mgv.io/scalable-architecture-demo

  110. Thank you! github.com/mgechev twitter.com/mgechev blog.mgechev.com