This talk contains silver bullets that can make enthusiats of the cargo cult addicted

@JulienTopcu

@JulienTopcu

?đŸ€Ș?

@JulienTopcu

I want to search for trains going to my destination

≠

I create a "Search" Resource

@JulienTopcu

Uniform Interface

Architectural Styles and the Design of Network-based Software Architectures - Roy Fielding

@JulienTopcu

Impedance mismatch

© Paramount Pictures - Interstellar

@JulienTopcu

© Paramount Pictures - Interstellar

Julien Topçu

Tech Coach

beyondxscratch.com

julien.topcu@owasp.org

craftsrecords.org

@JulienTopcu

@JulienTopcu

REST next level :
Crafting
Domain-Driven Designed web APIs

@JulienTopcu

PB#1 : False beliefs

© Lucasfilms - "STAR WARS: Revenge of the Sith"

@JulienTopcu

interface BookSpaceTrains {
    Booking fromTheSelectionOf(Search search)
}
POST /bookings

@JulienTopcu

interface RebookSpaceTrains {

    Booking atANewDate(UUID bookingId, LocalDate newdate)
}
interface BookSpaceTrains {
    Booking fromTheSelectionOf(Search search)
}
POST /bookings

@JulienTopcu

interface RebookSpaceTrains {

    Booking atANewDate(UUID bookingId, LocalDate newdate)
}
POST /bookings
interface BookSpaceTrains {
    Booking fromTheSelectionOf(Search search)
}
POST /bookings

@JulienTopcu

bookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /bookings
{
  "searchId" : "...",
}
POST /bookings
{
  "bookingId" : "...",
  "newDate": "..."
}

I book what I have selected

I'm rebooking the same train at a new date

Loss of the notion of Selection

Semantic confusion with the previous use case

@JulienTopcu

BookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /rebookings
{
  "searchId" : "...",
}
POST /bookings
{
  "bookingId" : "...",
  "newDate": "..."
}

@JulienTopcu

Re-booking

is an action, and not an entity

Pseudo-fonctionnel

@JulienTopcu

REST

must not lead

the design of your business domain !!!

@JulienTopcu

Pseudo-functional

Rebooking

Accidental Complexity

@JulienTopcu

POST /bookings/{id}/rebook
{
  "newDate": "..."
}
booking {
  "bookingId": "{newId}",
  "date": "{newDate}"
}
GET /bookings/{newId}

@JulienTopcu

Nouns VS Verbs

@JulienTopcu

Binding Semantics to URI

 At no time whatsoever do the server or client software need to know or understand the meaning of a URI -- they merely act as a conduit through which the creator of a resource (a human naming authority) can associate representations with the semantics identified by the URI

Architectural Styles and the Design of Network-based Software Architectures - Roy Fielding

@JulienTopcu

One of the secrets of the magic trick !

đŸ§™đŸżâ€â™‚ïž

@JulienTopcu

PB#2 :
Confusing model and business

© Lucasfilms - "STAR WARS: Revenge of the Sith"

@JulienTopcu

The business đŸ‘©â€đŸ’Œ

@JulienTopcu

Search
|_ availableSpaceTrains
|_ selection
  |_ selectedSpaceTrains
PATCH /searches/{id}/selection

The model 📐

@JulienTopcu

I select a fare on a train

≠

I update my "Selection" sub-resource into my "Search" resource

@JulienTopcu

In the worst case

@JulienTopcu

In the best case

@JulienTopcu

Accidental Complexity of adaptation

Exposing our model to our consumer

↔

@JulienTopcu

PB#3 :
The encapsulation betrayal

© Lucasfilms - "STAR WARS: Revenge of the Sith"

@JulienTopcu

class Search {

    UUID id
    Criteria criteria
    SpaceTrains spaceTrains
    Selection selection

    UUID getId() {...}
    void setId(UUID id) {...}
    Criteria getCriteria() {...}
    void setCriteria(Criteria criteria) {...}

    /* only getters and setters */
}

Anemic domain

@JulienTopcu

data class Search { //Aggregate
    /*attributes*/
    /*no setters*/
   
    fun changeCriteria(criteria : Criteria) : Search {
    	return this.copy(criteria = criteria, spaceTrains = emptyList())
    }
}

Domain-Driven Design

We encapsulate the states
to expose behaviors

@JulienTopcu

GET /searches/{id}
{
        "id": "c956d89b-73fc-49ce-a911-f0058c0672f6",
        "criteria": {/*...*/},
        "spaceTrains": [/*...*/],
        "selection": {/*...*/}
}

REST

We de-encapsulate the behaviors

to expose the states

@JulienTopcu

© Lucasfilms - "STAR WARS: Attack of the Clones"

@JulienTopcu

C

R

U

D

ompletely

idiculous &

seless

DD

@JulienTopcu

bookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /bookings
{
  "searchId" : "...",
}
POST /bookings
{
  "bookingId" : "...",
  "newDate": "..."
}

We have to read the payload to deduce the use case

Loss of meaning through de-encapsulation

@JulienTopcu

@JulienTopcu

PATCH /searches/{id}/selection
[
  { 
    "op": "add", 
    "path": "/selectedSpaceTrains/-", 
    "value": {"trainNumber" : "MOON421", "fare" : "FIRST"}
  }
]

Accidental Complexity of adaptation

De-encapsulation

@JulienTopcu

POST /searches/{id}/spaceTrains/{number}/fares/{code}/select
Search /* Domain Model */
|_ availableSpaceTrains
|_ selection
  |_ selectedSpaceTrains

@JulienTopcu

POST /searches/{id}/spaceTrains/{number}/fares/{code}/select

How does the front-end know it can do this ???

© Touchstone Pictures - "The Hitchhiker's Guide to the Galaxy"

@JulienTopcu

PB#4 : Anemic API

© Marvel Studio - "Guardians of the Galaxy Vol.1"

regarding the workflow...

@JulienTopcu

@JulienTopcu

POST /searches   /*RoundTrip Criteria Submitted*/

GET  /searches/{id}/spaceTrains?bound=OUTBOUND

POST /searches/{id}/spaceTrains/{number}/fares/{code}/select

GET  /searches/{id}/spaceTrains?bound=INBOUND

POST /searches/{id}/spaceTrains/{number}/fares/{code}/select

POST /bookings   /*Trip Booked*/

How do we know that we must proceed in this order?

@JulienTopcu

What is preventing the front-end from:

  • Displaying the inbound trains before the outbound ones?

  • Trying the retrieve inbound trains for a one way trip?

  • Calling the booking creation endpoint although the selection is not yet complete?

@JulienTopcu

Nothing because our APIs are anemic

Only the front-end is implementing the business workflow !

↔

@JulienTopcu

if(searchType == "roundtrip" && outBoundIsSelected && inBoundIsSelected){
  displayBookingButton()
}

@JulienTopcu

if(searchType == "roundtrip" && outBoundIsSelected && inBoundIsSelected){
  displayBookingButton()
}

Some parts of the business logic are leaking in the consumer...

@JulienTopcu

We must encapsulate the workflow inside the API !

© Paramount Pictures - "Star Trek: The Next Generation"

@JulienTopcu

© martinfowler.com - "Richardson Maturity Model"

@JulienTopcu

HATEOAS

A linking system (URLs) allowing you to:

  • Expose the relationships between the domain objects

  • Discover the domain

  • Encapsulate the business workflow

Hypermedia As The Engine of Application State

@JulienTopcu

HATEOAS

@JulienTopcu

The links

The front-end must not try to understand how the URLs are formed !

⇒

A better decoupling
between the consumption
and the implementation of the domain model

@JulienTopcu

  • False beliefs on the REST nomenclature
  • Using verbs enrich the semantic of the API
     
  • Straight exposition of our model in the API
  • Business-oriented endpoint on the back-end
     
  • Exposing the states of the domain objects
  • Encapsulation of behaviors attached to the responsible objects in the API
     
  • Lack of workflow definition (anemic API)
  • HATEOAS to drive workflow from the API

REST impedance mismatch ?

❌

❌

❌

❌

✔

✔

✔

✔

REST impedance mismatch ?

@JulienTopcu

  • False beliefs on the REST nomenclature
  • Using verbs enrich the semantic of the API
     
  • Straight exposition of our model in the API
  • Business-oriented endpoint on the back-end
     
  • Exposing the states of the domain objects
  • Encapsulation of behaviors attached to the responsible objects in the API
     
  • Lack of workflow definition (anemic API)
  • HATEOAS to drive workflow from the API

❌

❌

❌

❌

✔

✔

✔

✔

@JulienTopcu

Repository

@JulienTopcu

Thanks!đŸ™đŸ»

© Paramount Pictures - "Star Trek"

@JulienTopcu

© Kirsten Luce for The New York Times

@JulienTopcu

© Kirsten Luce for The New York Times

@JulienTopcu

© Kirsten Luce for The New York Times

@JulienTopcu

© Kirsten Luce for The New York Times

© Cottonbro - Pexels

C

R

U

D

@JulienTopcu

Je sélectionne un train et un tarif

≠

Je mets Ă  jour ma sous-ressource "Selection" dans ma ressource "Recherche"

@JulienTopcu

Outbound (Aller)

Inbound (Retour)

@JulienTopcu

Bound

Outbound ?

Inbound ?

???

@JulienTopcu

JourneyOrder

0

2

1

@JulienTopcu

BookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /bookings
{
  "searchId" : "...",
}
POST /bookings
{
  "bookingId" : "...",
  "newDate": "..."
}

@JulienTopcu

Criteria⚙

Search🔍

Selection✔

Ubiquitous Language

🚅

@JulienTopcu

Les Liens

Une étiquette sémantique + une URL

@JulienTopcu

Les Liens

Une étiquette sémantique + une URL

Le front ne doit pas chercher à comprendre comment les URLs sont formées !

@JulienTopcu

Ubiquitous Language

BookingđŸŽ«

@JulienTopcu

@JulienTopcu

interface SearchForSpaceTrains {
    Search satisfying(Criteria criteria)
}
POST /searches

@JulienTopcu

From the Earth to the Moon

@JulienTopcu

@JulienTopcu

POST /searches
GET /searches/{id}

🔍

@JulienTopcu

POST /searches
GET /searches/{id}

🔍

✔

PATCH /searches/{id}/selection
GET /searches/{id}/selection

@JulienTopcu

POST /searches
GET /searches/{id}
PATCH /searches/{id}/selection
GET /searches/{id}/selection
POST /bookings
GET /bookings/{id}

✔

🔍

đŸŽ«

@JulienTopcu

Dossier

âœˆïžđŸš…đŸš˜đŸš

@JulienTopcu

MetaDossier

âœˆïžđŸš…đŸš˜đŸš

@JulienTopcu

MetaDossier

LightweightDossier

@JulienTopcu

BookSpaceTrains.fromTheSelectionOf(search)
rebookSpaceTrains.atANewDate(bookingId,newdate)
POST /rebookings
{
  "searchId" : "...",
}
POST /bookings
{
  "bookingId" : "...",
  "newDate": "..."
}

@JulienTopcu

class Search {}
interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

class Search { //Aggregate
    /*attributes*/
    /*no setters*/
    Search(Selection selection, SpaceTrains spacetrains, ...){...}
    
    Search selectSpaceTrainWithFare(UUID spaceTrainId, UUID fareId) {}
    Boolean isSelectionComplete() {...}
}

interface SearchForSpaceTrains { //DomainService
    Search satisfying(Criteria criteria)
}

interface BookSpaceTrains { //DomainService
    Booking fromTheSelectionOf(Search thisSearch)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

???

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

interface ColumbiadExpressService {
    Boolean addSpaceTrain(UUID searchId,UUID spaceTrainId,UUID fareId)
    Boolean isSelectionComplete(UUID searchId)
}
interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}
interface ColumbiadExpressService {
    Boolean addSpaceTrain(UUID searchId,UUID spaceTrainId,UUID fareId)
    Boolean isSelectionComplete(UUID searchId)
}

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}
interface ColumbiadExpressService {
    Boolean addSpaceTrain(UUID searchId,UUID spaceTrainId,UUID fareId)
    Boolean isSelectionComplete(UUID searchId)
}

???

@JulienTopcu

interface SearchManager {
    Search createSearch(Criteria criteria)
    void updateSearch(UUID searchId, Selection selection)
    // Search <>-- SpaceTrains && Selection <>-- SpaceTrains
    void updateSearch(UUID searchId, SpaceTrains spaceTrains)
    Booking toBooking(UUID searchId)
}
interface ColumbiadExpressService {
    Boolean addSpaceTrain(UUID searchId,UUID spaceTrainId,UUID fareId)
    Boolean isSelectionComplete(UUID searchId)
}

@JulienTopcu

© Mickael Jackson - Thriller

@JulienTopcu

class Search {

    UUID id
    Criteria criteria
    SpaceTrains spaceTrains
    Selection selection

    UUID getId() {...}
    void setId(UUID id) {...}
    Criteria getCriteria() {...}
    void setCriteria(Criteria criteria) {...}

    /* only getters and setters */
}

@JulienTopcu

class Search {
  SpaceTrains getSpaceTrains(){}
  
  SpaceTrains getSpaceTrains(Bound bound){}  
    
  Selection getSelection() {}
  Boolean isSelectionComplete() {}
  
  Search selectSpaceTrainWithFare(...) {}
  Search selectSeatOptions(...) {}
}
GET /searches/{id}/spacetrains
GET /searches/{id}/spacetrains?bound={bound}
GET /searches/{id}/selection
GET /searches/{id}/selection/complete

@JulienTopcu

interface SearchForSpaceTrains {
    infix fun satisfying(criteria: Criteria): Search // POST /searches
}

interface BookSomeSpaceTrains {
    infix fun `from the selection of`(search: Search): Booking // POST /bookings
}


data class Search(/*...*/) {
  
  fun selectSpaceTrainWithFare(/*...*/) : Search // PUT /searches/{id}/selection
  fun selectSeatOptions(/*...*/) : Search // PUT /searches/{id}/selection
  fun isSelectionComplete() : Boolean // GET /searches/{id}/selection
  
}

@JulienTopcu

/*Search*/
{
        "id": "c956d89b-73fc-49ce-a911-f0058c0672f6",
        "criteria": {/*...*/},
        "spaceTrains": [/*...*/],
        "selection": {/*...*/}
}

@JulienTopcu

Les Liens

"selection": {
            "href": "http://localhost/searches/{id}/selection"
},

"create-booking": {
            "href": "http://localhost/bookings?searchId={id}"
},

"all-outbounds": {
            "href": "http://localhost/searches/{id}/spacetrains?journeyOrder=0&onlySelectable=false"
},

"outbounds-for-current-selection": {
            "href": "http://localhost/searches/{id}/spacetrains?journeyOrder=0&onlySelectable=true"
},

@JulienTopcu

ProblĂšmes Majeurs

Bucket GraphQL

FALCOR

@JulienTopcu

GRAPHQL

@JulienTopcu

?

+

@JulienTopcu

1999

"price" : {
  "amount" : 1000,
  "currency" : "FR"
}

=> 1000 FR

@JulienTopcu

2002

"price" : {
  "amount" : 150,
  "currency" : "EUR"
}

=> 150 FR

@JulienTopcu

CAS Train

+

Systeme référentiel

Data as a Service

@JulienTopcu

ComplexitĂ© accidentelle d'adaptation => Adaptation de isSelectionComplete de Search (aggreggat necessitant criteria) vers un /searches/../selection et les select* alors qu'ils devraient ĂȘtre niveau Search

@JulienTopcu

"Charge cognitive" => on se positionne sur une ressource selection dans l'API alors que l'utilisateur clique sur une fare dans un spacetrain

@JulienTopcu

Perte d'encapsulation mise a jours des Ă©tats

JSON + CRUD => API anémique

PUT ../selection pour SeatOptions & Fares

POST /searches et bookings qui peuvent ĂȘtre vide

Le workflow métier ne ressort pas

Faire une représentation event storming du workflow et montrer qu'on perd ca dans l'API
Techniquement on peut créer un booking avant la recherche

@JulienTopcu

Impedance Mismatch

@JulienTopcu

=> Logique déportée dans le consommateur exemple quand est-ce qu'on est pret a booker

@JulienTopcu

6.2.4 Binding Semantics to URI

As mentioned above, a resource can have many identifiers. In other words, there may exist two or more different URI that have equivalent semantics when used to access a server. It is also possible to have two URI that result in the same mechanism being used upon access to the server, and yet those URI identify two different resources because they don't mean the same thing.

Semantics are a by-product of the act of assigning resource identifiers and populating those resources with representations. At no time whatsoever do the server or client software need to know or understand the meaning of a URI -- they merely act as a conduit through which the creator of a resource (a human naming authority) can associate representations with the semantics identified by the URI. In other words, there are no resources on the server; just mechanisms that supply answers across an abstract interface defined by resources. It may seem odd, but this is the essence of what makes the Web work across so many different implementations.

C

R

U

D

omment

endre

seless

DD

@JulienTopcu

C

R

U

D

@JulienTopcu

interface SearchForSpaceTrains {
    infix fun satisfying(criteria: Criteria): Search
}

interface BookSomeSpaceTrains {
    infix fun `from the selection of`(search: Search): Booking
}


data class Search(/*...*/) {
  
  fun getSpaceTrainWithNumber(wantedNumber: String) : SpaceTrain {}
  fun selectSpaceTrainWithFare(spaceTrainNumber: String, fareId: UUID, resetSelection: Boolean) : Search {}
  fun isSelectionComplete() : Boolean {}
  fun selectableSpaceTrains(bound: Bound) : List<SpaceTrain> {}
  
}

?

 

?

C

R

U

D

@JulienTopcu

class Search {
  
  fun create() : Search {}
  fun read() : Search {}
  fun update() : Search {}
  fun delete() : Search {}
  
}

Paugeot Anemic

@JulienTopcu

© Peugeot - "Le modeleur"

@JulienTopcu

© The Simpsons - "Lisa's Pony"

Domain-Driven Design @ericevans0

@JulienTopcu

@JulienTopcu

@JulienTopcu

@JulienTopcu

C

R

U

D

REST next level : Crafting Domain-Driven Designed web APIs

By Julien Topçu

REST next level : Crafting Domain-Driven Designed web APIs

You have just coded your business logic by applying the principles of Domain-Driven Design! But when comes the time to write your API, you are facing a serious issue! All the intention and the expression of your domain go up in smoke to fit the blankness methods GET, POST, etc. Denatured by the REST layer, the business workflow is then deported on the consumer side to compensate for the limited vocabulary of this well-known CRUD protocol... During this talk, we will see how to bring the business intent back inside the REST API by finally being able to expose our domain services and agregates' methods. The business workflow will also be encapsulated in the REST API in order to have the power to guide our consumers through the workflow of our domain.

  • 2,116