.NET Day 2019 - Designing a UI for Microservices by Mauro Servienti

.NET Day 2019 - Designing a UI for Microservices by Mauro Servienti

How do we design a UI when the back-end system consists of dozens (or more) microservices? We have separation and autonomy on the back end, but on the front end this all needs to come back together. How do we stop it from turning into a mess of spaghetti code? How do we prevent simple actions from causing an inefficient torrent of web requests? Join Mauro in building a Composite UI for Microservices from scratch, using .NET Core. Walk away with a clear understanding of what Services UI Composition is and how you can architect front end to be Microservices ready.

E6cffbf3b7a5fbfee4707033ef1636f5?s=128

dotnetday

May 28, 2019
Tweet

Transcript

  1. designing a UI for Microservices Mauro Servienti mauroservienti

  2. spaghetti trademark is mine mauroservienti

  3. All I wanna do when I wake up in the

    morning is… Rosanna, Toto. Toto IV mauroservienti
  4. Buy a "Banana Protector" mauroservienti

  5. Does a page like that exist? mauroservienti

  6. There is no "Banana Protector" Sales Warehouse Shipping Catalog mauroservienti

  7. Domain Model Decomposition services owning their own piece of information

    Single Responsibility Principle mauroservienti
  8. How can we design something like that? mauroservienti

  9. Denormalization Temptations… Catalog Sales Shipping Warehouse De-normalized API Client “Product”

    ViewModel… /products/1 mauroservienti
  10. that’s a cache mauroservienti

  11. Oh…and by the way… mauroservienti

  12. Whatchoo talkin' 'bout, Willis? a report not a cache mauroservienti

  13. It's a reporting and integration issue • We're crossing a

    “boundary” • Data flow out of each service to the user • Users are already pulling things on demand • let's benefit of that mauroservienti
  14. ViewModel Composition /products/ Catalog Sales Shipping Warehouse Client PK PK

    PK PK 1 ViewModel… mauroservienti
  15. ViewModel Composition Catalog Sales Shipping Warehouse composition gateway HTTP Request

    -> /products/1 Catalog Handler Sales Handler Shipping Handler Warehouse Handler Request matching HTTP Response -> ViewModel Composition Composed ViewModel mauroservienti Creates empty ViewModel
  16. Request Handling mauroservienti class ProductDetailsGetHandler : IHandleRequests { }

  17. Request Matching mauroservienti class ProductDetailsGetHandler : IHandleRequests { } public

    bool Matches(RouteData routeData, string httpVerb, HttpRequest request) { } var controller = (string)routeData.Values["controller"]; return HttpMethods.IsGet(httpVerb) && controller.ToLowerInvariant() == "products" && routeData.Values.ContainsKey("id");
  18. Composition mauroservienti class ProductDetailsGetHandler : IHandleRequests { } public bool

    Matches(RouteData routeData, string httpVerb, HttpRequest request) { } public async Task Handle(string requestId, dynamic vm, RouteData routeData, HttpRequest request) { var id = (string)routeData.Values["id"]; var url = $"http://localhost:5002/api/product-details/product/{id}"; var response = await new HttpClient().GetAsync(url); dynamic details = await response.Content.AsExpando(); vm.ProductName = details.Name; vm.ProductDescription = details.Description; }
  19. Composition mauroservienti class ProductDetailsGetHandler : IHandleRequests { } public bool

    Matches(RouteData routeData, string httpVerb, HttpRequest request) { } public async Task Handle(string requestId, dynamic vm, RouteData routeData, HttpRequest request) { var id = (string)routeData.Values["id"]; var url = $"http://localhost:5002/api/product-details/product/{id}"; var response = await new HttpClient().GetAsync(url); dynamic details = await response.Content.AsExpando(); vm.ProductName = details.Name; vm.ProductDescription = details.Description; } retrieve data from your favorite source
  20. Output mauroservienti { "productName": "Banana Holder", "productDescription": "Outdoor travel cute

    banana protector storage box", "productPrice": 10, "productShippingOptions": "Express Delivery, Regular mail", "productInventory": 4, "productOutOfStock": false }
  21. Output mauroservienti { "productName": "Banana Holder", "productDescription": "Outdoor travel cute

    banana protector storage box", "productPrice": 10, "productShippingOptions": "Express Delivery, Regular mail", "productInventory": 4, "productOutOfStock": false } Catalog Sales Shipping Warehouse
  22. is all what glitters gold? mauroservienti

  23. What about grids? mauroservienti

  24. ProductNameA € 20.00 cover image A Supplier A ProductNameB €

    20.00 cover image B Supplier B ProductNameC € 20.00 cover image C Supplier C ProductNameD € 20.00 cover image D Supplier D mauroservienti View Model /available/products
  25. Catalog Handler View Model Catalog Handler ProductNameA € 20.00 cover

    image A Supplier A ProductNameB € 20.00 cover image B Supplier B ProductNameC € 20.00 cover image C Supplier C ProductNameD € 20.00 cover image D Supplier D load available products mauroservienti /available/products
  26. client-side message broker Sales Handler “….” Handler Sales Handler “….”

    Handler ProductNameA € 20.00 cover image A Supplier A ProductNameB € 20.00 cover image B Supplier B ProductNameC € 20.00 cover image C Supplier C ProductNameD € 20.00 cover image D Supplier D publish ` AvailableProductsLoaded` receive event mauroservienti Catalog Handler View Model Catalog Handler /available/products receive event
  27. ProductNameA € 20.00 cover image A Supplier A ProductNameB €

    20.00 cover image B Supplier B ProductNameC € 20.00 cover image C Supplier C ProductNameD € 20.00 cover image D Supplier D mauroservienti Sales Handler “….” Handler Sales Handler “….” Handler Catalog Handler View Model Catalog Handler /available/products
  28. Request Handling mauroservienti class AvailableProductsGetHandler : IHandleRequests { }

  29. Composition mauroservienti class AvailableProductsGetHandler : IHandleRequests { } public async

    Task Handle(string requestId, dynamic vm, RouteData routeData, HttpRequest request) { var url = $"http://localhost:5002/api/available/products"; var client = new HttpClient(); var response = await client.GetAsync(url); var availableProducts = await response.Content.As<int[]>(); var availableProductsViewModel = MapToDictionary(availableProducts); await vm.RaiseEvent(new AvailableProductsLoaded() { AvailableProductsViewModel = availableProductsViewModel }); vm.AvailableProducts = availableProductsViewModel.Values.ToList(); }
  30. Composition mauroservienti var availableProducts = await response.Content.As<int[]>(); var availableProductsViewModel =

    MapToDictionary(availableProducts); await vm.RaiseEvent(new AvailableProductsLoaded() { AvailableProductsViewModel = availableProductsViewModel }); vm.AvailableProducts = availableProductsViewModel.Values.ToList();
  31. Event Subscribers class AvailableProductsLoadedSubscriber : ISubscribeToCompositionEvents { } mauroservienti

  32. Composition class AvailableProductsLoadedSubscriber : ISubscribeToCompositionEvents { } public void Subscribe(IPublishCompositionEvents

    publisher) { publisher.Subscribe<AvailableProductsLoaded>(async (requestId, pageViewModel, @event, routeData, request) => { }); } mauroservienti
  33. Composition } async (requestId, pageViewModel, @event, routeData, request) => {

    var ids = string.Join(",", @event.AvailableProductsViewModel.Keys); var url = $"http://localhost:5001/api/prices/products/{ids}"; var client = new HttpClient(); var response = await client.GetAsync(url); dynamic[] productPrices = await response.Content.AsExpandoArray(); foreach (dynamic pp in productPrices) { @event.AvailableProductsViewModel[(int)productPrice.Id].ProductPrice = pp.Price; } }); mauroservienti
  34. Output mauroservienti { "avalableProducts": [{ "id": 1, "productName": "Banana Holder",

    "productDescription": "Outdoor travel cute banana protector storage box", "productPrice": 10, "inventory": 4, },{ "id": 2, "productName": "Nokia Lumia 635", "…": "…", }] }
  35. Output mauroservienti { "avalableProducts": [{ "id": 1, "productName": "Banana Holder",

    "productDescription": "Outdoor travel cute banana protector storage box", "productPrice": 10, "inventory": 4, },{ "id": 2, "productName": "Nokia Lumia 635", "…": "…", }] } Catalog Sales Warehouse
  36. ViewModel Composition Catalog Sales Shipping Warehouse composition gateway HTTP Request

    -> /products/1 Catalog Handler Sales Handler Shipping Handler Warehouse Handler HTTP Response -> ViewModel mauroservienti Request matching Composition Composed ViewModel Creates empty ViewModel
  37. Composed ViewModel ViewModel Composition Catalog Sales Shipping Warehouse composition gateway

    HTTP Request -> /products/1 HTTP Response -> ViewModel mauroservienti Creates empty ViewModel Request matching Composition Catalog Handler Sales Handler Shipping Handler Warehouse Handler
  38. Vertical ownership Catalog Sales Shipping Warehouse mauroservienti

  39. Vertical ownership Catalog Sales Shipping Warehouse Doc DB + HTTP

    DB DB back-end back-end Web Proxy API API Proxy to 3° party API 1 month cache 24 hours cache No cache 10 minutes cache User Interface mauroservienti
  40. Vertical ownership Catalog Sales Shipping Warehouse Doc DB + HTTP

    DB DB back-end back-end Web Proxy API API Proxy to 3° party API User Interface composition gateway Catalog Handler Sales Handler Shipping Handler Warehouse Handler mauroservienti
  41. The elephant in the room Catalog Sales Shipping Warehouse Doc

    DB + HTTP DB DB back-end back-end Web Proxy API API Proxy to 3° party API User Interface composition gateway Catalog Handler Sales Handler Shipping Handler Warehouse Handler mauroservienti
  42. Branding mauroservienti

  43. Branding • It's the technical authority owing the UI contract

    • Services that want to display data implement it • UI structure is defined as a “monolith” mauroservienti
  44. <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">@Model.ProductDescription</h3> </div> <div class="panel-body">

    <div>Price: @Model.ProductPrice</div> <div>Shipping Options: @Model.ProductShippingOptions</div> <div>Inventory: @Model.ProductInventory</div> </div> </div> mauroservienti
  45. magic UI Composition mauroservienti

  46. UI Composition •UI defines only high level structure •Highly coupled

    with the presentation technology •Branding is less important •The more “magic” the less Branding •Services own the whole vertical slice mauroservienti
  47. <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">@Model.ProductDescription</h3> </div> <div class="panel-body">

    @await Component.InvokeAsync("ProductDetailsPrice") @await Component.InvokeAsync("ProductDetailsShippingOptions") @await Component.InvokeAsync("ProductDetailsInventory") </div> </div> mauroservienti
  48. <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">@Model.ProductDescription</h3> </div> <div class="panel-body">

    @await Component.InvokeAsync("ProductDetailsPrice") @await Component.InvokeAsync("ProductDetailsShippingOptions") @await Component.InvokeAsync("ProductDetailsInventory") </div> </div> mauroservienti Catalog Sales Shipping Warehouse
  49. Full vertical ownership mauroservienti Catalog Sales Shipping Warehouse Doc DB

    + HTTP DB DB back-end back-end Web Proxy API API Proxy to 3° party API User Interface composition gateway Catalog Handler Sales Handler Shipping Handler Warehouse Handler
  50. Full vertical ownership client shell mauroservienti Catalog Components Sales Components

    Shipping Components Warehouse Components Catalog Sales Shipping Warehouse Doc DB + HTTP DB DB back-end back-end Web Proxy API API Proxy to 3° party API composition gateway Catalog Handler Sales Handler Shipping Handler Warehouse Handler
  51. Every year is getting shorter never seem to find the

    time… ViewModel Composition Demos bit.ly/dnd19-composition Udi Dahan about Service Modelling go.particular.net/dnd19-composition mauroservienti
  52. Demos: bit.ly/dnd19-composition Videos: go.particular.net/dnd19-composition Is it worth the effort? •

    Complexity increases as “black magic” kicks in • For large projects with a small UI team a traditional monolithic UI approach might be better mauroservienti
  53. Demos: bit.ly/dnd19-composition Videos: go.particular.net/dnd19-composition Is it worth the effort? •

    Complexity increases as “black magic” kicks in • It’s simpler to break a monolithic UI later • Than to manage not needed complexity mauroservienti
  54. Demos: bit.ly/dnd19-composition Videos: go.particular.net/dnd19-composition Takeaways • Boundaries are key to

    success • Do not bring in more technology to solve non- technical problems mauroservienti
  55. • Boundaries are key to success • Mental model can

    badly influence design • Users/Business analysts tend to think in term of data presentation mauroservienti Takeaways Demos: bit.ly/dnd19-composition Videos: go.particular.net/dnd19-composition
  56. Demos: bit.ly/dnd19-composition Videos: go.particular.net/dnd19-composition Takeaways • Boundaries are key to

    success • Mental model can badly influence design • Use ViewModel composition to present data • No need for complex projections and read models mauroservienti
  57. Mauro Servienti Solution Architect @ Particular Software the makers of

    NServiceBus mauro.servienti@particular.net @mauroservienti //github.com/mauroservienti //milestone.topics.it mauroservienti
  58. Thank you! Join me at the Particular booth Demos: bit.ly/dnd19-composition

    Videos: go.particular.net/dnd19-composition mauroservienti