Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Centralizing Business Logic on the Server: How We Designed Presentation APIs

Ricardo Lage
November 15, 2018

Centralizing Business Logic on the Server: How We Designed Presentation APIs

One of the challenges in engineering a software product today is to minimize the amount of code that needs to be duplicated to support the app on multiple platforms. This challenge is partly due to APIs that expose data as opposed to business content. With data-driven APIs the app must embed large chunks of the business logic. First, it must transform the data into content to display. Second, it must transform it back into data to be processed by the API. While solutions exist to share this transformation logic between multiple app platforms, we decided to host this logic on the server.

This is achieved by exposing a presentation API containing use case specific endpoints. Its responses have the content matching the needs of what is displayed by the app. This decision of building the product around a presentation API has served us well in this project. Throughout the life of our project, we have had the opportunity to exploit the presentation layer in the backend. This allowed us to iterate faster with the product by displaying different things without having to ship any new code to the front end, while reducing the apps development time.

Presentation done together with Antoine Tollenaere

Ricardo Lage

November 15, 2018
Tweet

More Decks by Ricardo Lage

Other Decks in Technology

Transcript

  1. A typical startup with a consumer product • Single product

    on 2 platforms: Android & iOS • Backend and frontend designed from the ground up • Small team, time and money constraints • Unproven market, need to iterate fast
  2. • Client server architecture • Server: computation and persisting states

    • Client: user experience • API design is a source of friction from day 1: requires discussion between engineers with different responsibilities Server iOS API Android
  3. { "trips": [ { "id": "100200300", "departure_datetime": "2018-11-07T19:00:00+01:00", "type": "COMMUTE_TRIP",

    "state": "WAITING_PASSENGERS", "departure_address": { "country_code": "FR", "city": "Paris", "line1": "6 Rue Menars", "line2": "Paris, France", "place_id": "g:ChIJ9XUaujtu5kcRVnLtJaC0T_g", "country": "France", "location": { "latitude": 48.8697709, "longitude": 2.3379789 }, "type": "HOME", "postal_code": "75002" }, "arrival_address": { "country_code": "FR", "city": "Versailles", "line1": "Chateau de Versailles", "line2": "Versailles, France", "place_id": "g:ChIJdUyx15R95kcRj85ZX8H8OAU", "country": "France", "location": { "latitude": 48.8048649, "longitude": 2.1203554 }, "type": "WORK", "postal_code": "78000" }, "route_polyline": "ahrkH}csWdBiChAgAfCoBVZr@xDzAu…" } ] } GET /trips
  4. { "trips": [ { ... 
 "departure_datetime": "2018-11-06T19:00:00+01:00", "state": "COMPLETED",

    ... }, { ... 
 "departure_datetime": "2018-11-07T19:00:00+01:00", "state": "WAITING_PASSENGERS", ... }, { ... 
 "departure_datetime": "2018-11-08T09:30:00+01:00", "state": "WAITING_PASSENGERS", ... }, { ... 
 "departure_datetime": "2018-11-08T19:00:00+01:00", "state": "CANCELLED", ... }, { ... 
 "departure_datetime": "2018-11-08T19:30:00+01:00", "state": "WAITING_PASSENGERS", ... }, { ... 
 "departure_datetime": "2018-11-09T09:30:00+01:00", "state": "WAITING_PASSENGERS", ... } ] } GET /trips
  5. { "trips": [ { ... 
 "departure_datetime": “2018-11-06T19:00:00+01:00", "state": "COMPLETED",

    ... }, { ... 
 "departure_datetime": “2018-11-07T19:00:00+01:00", "state": “WAITING_PASSENGERS”, ... }, { ... 
 "departure_datetime": "2018-11-08T09:30:00+01:00", "state": "WAITING_PASSENGERS", ... }, { ... 
 "departure_datetime": “2018-11-08T19:00:00+01:00", "state": "CANCELLED", ... }, { ... 
 "departure_datetime": "2018-11-08T19:30:00+01:00", "state": "WAITING_PASSENGERS", ... }, { ... 
 "departure_datetime": "2018-11-09T09:30:00+01:00", "state": "WAITING_PASSENGERS", ... } ] } GET /trips
  6. { "trips": [ GET /trips? from=2018-11-07T19:00:00+01:00 &states=WAITING_PASSENGERS { ... 


    "departure_datetime": "2018-11-07T19:00:00+01:00", "state": "WAITING_PASSENGERS", ... }, { ... 
 "departure_datetime": "2018-11-08T09:30:00+01:00", "state": "WAITING_PASSENGERS", ... }, { ... 
 "departure_datetime": "2018-11-08T19:00:00+01:00", "state": "CANCELLED", ... }, { ... 
 "departure_datetime": "2018-11-06T19:00:00+01:00", "state": "COMPLETED", ... }, { ... 
 "departure_datetime": "2018-11-08T19:30:00+01:00", "state": "WAITING_PASSENGERS", ... }, { ... 
 "departure_datetime": "2018-11-09T09:30:00+01:00", "state": "WAITING_PASSENGERS", ... } ] }
  7. { "trips": [ GET /trips? from=2018-11-07T19:00:00+01:00 &states=WAITING_PASSENGERS { ... 


    "departure_datetime": "2018-11-07T19:00:00+01:00", "state": "WAITING_PASSENGERS", ... }, { ... 
 "departure_datetime": "2018-11-08T09:30:00+01:00", "state": "WAITING_PASSENGERS", ... }, { ... 
 "departure_datetime": "2018-11-08T19:30:00+01:00", "state": "WAITING_PASSENGERS", ... }, { ... 
 "departure_datetime": "2018-11-09T09:30:00+01:00", "state": "WAITING_PASSENGERS", ... } ] }
  8. { "requests": [ { "id": 122001, "state": "CANCELLED_BY_PASSENGER", "passenger_id": 7839624,

    "number_of_seats": 2, "driver_remuneration": { "amount": 2, "currency": "EUR" }, "pickup_point": { ... }, "pickup_time": "2018-11-08T09:30:00+01:00", "dropoff_point": { ... } ... }, ... ] } Trip 1 Trip 2 Trip N … GET /trip/{1}/requests
  9. { "requests": [ { "id": 122001, "state": "CANCELLED_BY_PASSENGER", "passenger_id": 7839624,

    "number_of_seats": 2, "driver_remuneration": { "amount": 2, "currency": "EUR" }, "pickup_point": { ... }, "pickup_time": "2018-11-08T09:30:00+01:00", "dropoff_point": { ... } ... }, ... ] } GET /trip/{1}/requests Trip 1 Trip 2 Trip N …
  10. { "requests": [ { "id": 122001, "state": "CANCELLED_BY_PASSENGER", "passenger_id": 7839624,

    "number_of_seats": 2, "driver_remuneration": { "amount": 2, "currency": "EUR" }, "pickup_point": { ... }, "pickup_time": "2018-11-08T09:30:00+01:00", "dropoff_point": { ... } ... }, ... ] } GET /trip/{1}/requests ?state=WAITING_DRIVER Trip 1 Trip 2 Trip N …
  11. GET /trip/{N}/requests ?state=WAITING_DRIVER Trip 1 Trip 2 Trip N …

    { "requests": [ { "id": 301009, "state": “WAITING_DRIVER”, "passenger_id": 7839624, "number_of_seats": 2, "driver_remuneration": { "amount": 2, "currency": "EUR" }, "pickup_point": { ... }, "pickup_time": "2018-11-08T09:30:00+01:00", "dropoff_point": { ... } ... }, ... ] }
  12. { "requests": [ { "id": 301009, "state": “WAITING_DRIVER”, "passenger_id": 7839624,

    "number_of_seats": 2, "driver_remuneration": { "amount": 2, "currency": "EUR" }, "pickup_point": { ... }, "pickup_time": "2018-11-08T09:30:00+01:00", "dropoff_point": { ... } ... }, ... ] }
  13. { "requests": [ { "id": 301009, "state": “WAITING_DRIVER”, "passenger_id": 7839624,

    "number_of_seats": 2, "driver_remuneration": { "amount": 2, "currency": "EUR" }, "pickup_point": { ... }, "pickup_time": "2018-11-08T09:30:00+01:00", "dropoff_point": { ... } ... }, ... ] } GET /user/{7839624}/ { "first_name": "Ricardo", "age": 32, "photo_url": "...", "phone_number": { "country_number": "32", "user_number": "27541253" }, ... }
  14. Client work • Filter the server requests based on the

    user screen (e.g. home vs history) • Multiple server requests to get all data needed (e.g., trips, passenger requests and users) • Transformation logic to display content • Show only the required fields (e.g., trip summaries in the home) • Grouping content (e.g., by date) • Actions to take (e.g., display new screen with passenger request if present) • Handling permissions (e.g. whether to show the user’s phone number or not)
  15. Group N Group 1 All trips Data GET /trips?from=2018-11-07T19:00:00+01:00 &states=WAITING_PASSENGERS

    Trip 1 Trip 2 Trip N … Presenter Grouped trip summaries Trip Summary 1 Trip Summary 2 Trip Summary N … View
  16. Group N Group 1 All trips Data GET /trips?from=2018-11-07T19:00:00+01:00 &states=WAITING_PASSENGERS

    Trip 1 Trip 2 Trip N … Presenter Grouped trip summaries Trip Summary 1 Trip Summary 2 Trip Summary N … View ?
  17. { "trips": [ { "id": "100200300", "departure_datetime": "2018-11-07T19:00:00+01:00", "type": "COMMUTE_TRIP",

    "state": "WAITING_PASSENGERS", "departure_address": { "country_code": "FR", "city": "Paris", "line1": "6 Rue Menars", "line2": "Paris, France", "place_id": "g:ChIJ9XUaujtu5kcRVnLtJaC0T_g", "country": "France", "location": { "latitude": 48.8697709, "longitude": 2.3379789 }, "type": "HOME", "postal_code": "75002" }, "arrival_address": { "country_code": "FR", "city": "Versailles", "line1": "Chateau de Versailles", "line2": "Versailles, France", "place_id": "g:ChIJdUyx15R95kcRj85ZX8H8OAU", "country": "France", "location": { "latitude": 48.8048649, "longitude": 2.1203554 }, "type": "WORK", "postal_code": "78000" }, "route_polyline": "ahrkH}csWdBiChAgAfCoBVZr@xDzAu…" } ] } GET /trips? from=2018-11-07T19:00:00+01:00 &states=WAITING_PASSENGERS
  18. { “trip_summaries”: [ { "id": "100200300", "departure_datetime": "2018-11-07T19:00:00+01:00", "type": "COMMUTE_TRIP",

    "state": "WAITING_PASSENGERS", "departure_address": { "country_code": "FR", "city": "Paris", "line1": "6 Rue Menars", "line2": "Paris, France", "place_id": "g:ChIJ9XUaujtu5kcRVnLtJaC0T_g", "country": "France", "location": { "latitude": 48.8697709, "longitude": 2.3379789 }, "type": "HOME", "postal_code": "75002" }, "arrival_address": { "country_code": "FR", "city": "Versailles", "line1": "Chateau de Versailles", "line2": "Versailles, France", "place_id": "g:ChIJdUyx15R95kcRj85ZX8H8OAU", "country": "France", "location": { "latitude": 48.8048649, "longitude": 2.1203554 }, "type": "WORK", "postal_code": "78000" }, "route_polyline": "ahrkH}csWdBiChAgAfCoBVZr@xDzAu…" } ] } GET /home/trips
  19. { “trip_summaries”: [ { "id": "100200300", “departure_time”: “7:00 PM”, “title”:

    “Work → Home“, “subtitle”: “Waiting for passengers" } ] } GET /home/trips
  20. { “trip_summaries”: [ { "id": "100200300", “departure_time”: “7:00 PM”, “title”:

    “Work → Home“, “subtitle”: “Waiting for passengers" } ] } GET /home/trips • Formatting of content done server-side • Authorization and Privacy: Client receives only the content that the user is allowed to see • Easier to add than to remove fields • No need to map server objects to view objects • More compact responses
  21. { “trip_summaries”: [ { "id": "100200300", “departure_time”: “7:00 PM”, “title”:

    “Work → Home“, "subtitle": “Waiting for passengers" } ] } GET /home/trips
  22. { “trip_summaries”: [ { "id": "100200300", “departure_time”: “7:00 PM”, “title”:

    “Work → Home“, "subtitle": “Waiting for passengers", "menu_actions": ["CANCEL", "CHANGE_TIME"] } ] } GET /home/trips
  23. { "groups": [ { "title": "Today", "trip_summary_ids": [ "100200300" ]

    } ], “trip_summaries”: [ { "id": "100200300", “departure_time”: “7:00 PM”, “title”: “Work → Home“, "subtitle": “Waiting for passengers", "menu_actions": ["CANCEL", "CHANGE_TIME"] } ] } GET /home/trips
  24. { "groups": [ { "title": "Today", "trip_summary_ids": [ "100200300" ]

    } ], “trip_summaries”: [ { "id": "100200300", “departure_time”: “7:00 PM”, “title”: “Work → Home“, "subtitle": “Waiting for passengers", "menu_actions": ["CANCEL", “CHANGE_TIME”], "active_request": { "id": "301009", "passenger_id": "398539", "driver_remuneration": "€ 2", "pickup_point": "6 rue menars", "pickup_time": "7:30 AM", "dropoff_point": "Chateau Versailles" } } ], "users": [ { "id": "398539", "name": "Ricardo", ... } ] } GET /home/trips
  25. { "groups": [ { "title": "Today", "trip_summary_ids": [ "100200300" ]

    } ], “trip_summaries”: [ { "id": "100200300", “departure_time”: “7:00 PM”, “title”: “Work → Home“, "subtitle": “Waiting for passengers", "menu_actions": ["CANCEL", “CHANGE_TIME”], "active_request": { "id": "301009", "passenger_id": "398539", "driver_remuneration": "€ 2", "pickup_point": "6 rue menars", "pickup_time": "7:30 AM", "dropoff_point": "Chateau Versailles" } } ], "users": [ { "id": "398539", "name": "Ricardo", ... } ] } GET /home/trips
  26. Conclusion: for the clients • Simplified client-side business logic →

    easier to develop, test and maintain • Less “patch" updates on the app stores -> Some of the product changes could now be done directly on the server • More focus can be put on the UX
  27. Conclusion: for the server • Boundaries between domain layer and

    presentation layer is not enforced • Lots of glue code for presentation • Server code needs good support for composing objects from various sources and format them to the API • API is tied to the product — inappropriate as a public platform API
  28. Conclusion: for the team • Client developers “own” the API

    • They have to get involved in the presentation layer of the server code • Backend developers can still focus on logic specific to the business needs (algorithms, etc) • Works well for a small team — not necessarily scalable