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