Cartographie de contextes

VoletStratégique Les frontières ne suffisent pas : reste à savoir qui dépend de qui, et à quelles conditions.

Une carte de contextes représente les contextes délimités d’un système et la nature de leurs relations. Sa particularité, et ce qui la rend inhabituelle pour un artefact technique, est qu’elle décrit deux choses à la fois : un flux de données et un rapport de force organisationnel. Les deux comptent, et le second est souvent le plus déterminant.

Amont et aval

Le vocabulaire de base tient en deux mots. Le contexte amont (upstream) influence l’aval ; le contexte aval (downstream) subit. La formule d’Evans est plus imagée : l’amont mange, l’aval fait la vaisselle.

Ce qui compte n’est pas le sens des appels réseau. Un contexte peut appeler un autre tout en étant en amont de lui — si c’est lui qui décide du format et du calendrier, il est en amont. La question est : qui doit s’adapter à qui quand l’un des deux change ?

Cette lecture explique pourquoi une intégration techniquement simple peut être un cauchemar : si l’amont est une équipe qui ne vous connaît pas, dans une autre direction, avec ses propres priorités, aucun choix de protocole ne compensera.

Les neuf relations

Evans catalogue neuf patrons. Trois décrivent une coopération, trois une soumission plus ou moins protégée, deux une offre publique, un l’absence de relation.

PatronRapportEn une phrase
PartenariatSymétriqueLes deux équipes réussissent ou échouent ensemble.
Noyau partagéSymétriqueUn sous-ensemble de modèle partagé et co-détenu.
Client / FournisseurAsymétrique, négociéLes besoins de l’aval entrent dans le planning de l’amont.
ConformisteAsymétrique, subiL’aval adopte le modèle de l’amont sans traduire.
Couche d’anticorruptionAsymétrique, protégéL’aval traduit pour préserver son modèle.
Service ouvertAmont généreuxL’amont publie un protocole stable pour tous.
Langage publiéAmont généreuxUn format d’échange documenté et versionné.
Chemins séparésAucunPas d’intégration, assumée.
Grande boule de boueConstatUne zone dont on ne modélise pas l’intérieur.

Partenariat

Deux contextes dont les évolutions sont interdépendantes, portés par deux équipes qui se coordonnent : livraisons synchronisées, tests d’intégration communs, décisions prises ensemble.

C’est efficace et cher. Le coût de coordination est permanent, et il croît avec la distance organisationnelle. Un partenariat qui s’étend au-delà de deux ou trois équipes cesse d’en être un : personne ne se coordonne plus, et l’on obtient un couplage sans le bénéfice de la coopération.

Noyau partagé

Un sous-ensemble du modèle — quelques objets valeur, des identifiants, un vocabulaire — est partagé sous forme de code commun, détenu par les deux équipes.

C’est le patron le plus puissant et le plus dangereux du catalogue. Puissant parce qu’il supprime toute traduction ; dangereux parce que toute modification du noyau exige un accord, et qu’un noyau qui grossit finit par recréer le modèle unifié qu’on cherchait à éviter.

Deux règles pour survivre : le garder petit — des identifiants et des objets valeur stables, jamais des agrégats ni des règles métier — et le versionner explicitement.

Client / Fournisseur

L’amont fournit, l’aval consomme, et l’aval a un droit de parole : ses besoins entrent dans les arbitrages de l’amont. C’est la relation saine par défaut entre deux équipes de la même organisation.

Elle suppose un arbitre commun. Sans quoi elle dégénère silencieusement en conformisme — l’aval continue de demander, l’amont continue de ne pas prioriser.

Conformiste

L’aval renonce à son propre modèle et adopte celui de l’amont tel quel, sans couche de traduction.

C’est présenté comme un renoncement, mais c’est parfois le bon choix. Se conformer au modèle de Stripe pour un contexte de paiement génerique, ou au format d’un standard sectoriel, économise une traduction pour un modèle dont on n’avait de toute façon pas besoin qu’il soit à nous. Le critère est simple : le conformisme est raisonnable dans un sous-domaine génerique, et rarement acceptable dans le cœur — voir Sous-domaines.

Couche d’anticorruption

L’aval construit une couche de traduction dédiée, dont l’unique rôle est d’empêcher le modèle de l’amont de contaminer le sien. C’est le patron le plus utile en pratique, en particulier face à un système hérité.

​
// Le port, exprimé dans le langage de NOTRE contexte
export interface ReferentielTarifaire {
  tarifApplicable(offre: OffreId, date: DateEffet): Promise<Tarif>;
}

// La couche d'anticorruption : tout le vocabulaire étranger s'arrête ici
export class ReferentielTarifaireViaProgicielHerite implements ReferentielTarifaire {
  constructor(private readonly soap: ClientSoapGescom) {}

  async tarifApplicable(offre: OffreId, date: DateEffet): Promise<Tarif> {
    const reponse = await this.soap.getPRIXART({
      CD_ART: offre.codeLegacy(),        // Notre identité → leur code article
      DT_DEB: formatAAAAMMJJ(date),
      IND_TYP: 'T',                      // Constante magique de 1998
    });

    if (reponse.COD_RET !== '0000') {    // Leur convention d'erreur
      throw new TarifIndisponible(offre, date);
    }

    return new Tarif(                    // Leur structure → notre modèle
      Montant.depuisCentimes(reponse.MNT_HT_CTS),
      TauxTva.depuisCode(reponse.CD_TVA),
    );
  }
}
from typing import Protocol


# Le port, exprimé dans le langage de NOTRE contexte
class ReferentielTarifaire(Protocol):
    def tarif_applicable(self, offre: OffreId, date: DateEffet) -> Tarif: ...


# La couche d'anticorruption : tout le vocabulaire étranger s'arrête ici
class ReferentielTarifaireViaProgicielHerite:
    def __init__(self, soap: ClientSoapGescom) -> None:
        self._soap = soap

    def tarif_applicable(self, offre: OffreId, date: DateEffet) -> Tarif:
        reponse = self._soap.get_prixart(
            CD_ART=offre.code_legacy(),      # Notre identité → leur code article
            DT_DEB=date.strftime("%Y%m%d"),
            IND_TYP="T",                     # Constante magique de 1998
        )

        if reponse.COD_RET != "0000":        # Leur convention d'erreur
            raise TarifIndisponible(offre, date)

        return Tarif(                        # Leur structure → notre modèle
            montant_ht=Montant.depuis_centimes(reponse.MNT_HT_CTS),
            taux_tva=TauxTva.depuis_code(reponse.CD_TVA),
        )
// Le port, exprimé dans le langage de NOTRE contexte
pub trait ReferentielTarifaire {
    fn tarif_applicable(&self, offre: OffreId, date: DateEffet)
        -> Result<Tarif, TarifIndisponible>;
}

// La couche d'anticorruption : tout le vocabulaire étranger s'arrête ici
pub struct ReferentielTarifaireViaProgicielHerite {
    soap: ClientSoapGescom,
}

impl ReferentielTarifaire for ReferentielTarifaireViaProgicielHerite {
    fn tarif_applicable(&self, offre: OffreId, date: DateEffet)
        -> Result<Tarif, TarifIndisponible>
    {
        let reponse = self.soap.get_prixart(GetPrixArtParams {
            cd_art: offre.code_legacy(),      // Notre identité → leur code article
            dt_deb: date.format("%Y%m%d").to_string(),
            ind_typ: "T",                     // Constante magique de 1998
        });

        if reponse.cod_ret != "0000" {        // Leur convention d'erreur
            return Err(TarifIndisponible { offre, date });
        }

        Ok(Tarif {                            // Leur structure → notre modèle
            montant_ht: Montant::depuis_centimes(reponse.mnt_ht_cts),
            taux_tva: TauxTva::depuis_code(&reponse.cd_tva),
        })
    }
}

Trois propriétés à remarquer. Le port ReferentielTarifaire ne contient aucun terme du système amont — c’est le critère de réussite. Le IND_TYP: 'T', le COD_RET, le format de date : tout cela existe dans exactement un fichier, et le jour où le progiciel est remplacé, c’est ce fichier qu’on jette. Enfin, l’erreur technique est traduite en concept métier — TarifIndisponible — au lieu de laisser remonter un code de retour.

Tip

Une couche d’anticorruption est un Adapter appliqué à une frontière de contexte, et c’est exactement le rôle d’un adaptateur sortant en architecture hexagonale. Les trois vocabulaires décrivent la même chose à trois échelles : un objet, une couche, un contexte.

Service ouvert et langage publié

Les deux vont généralement ensemble et concernent l’amont plutôt que l’aval.

Un service ouvert est un protocole conçu pour être consommé par plusieurs contextes, et stable — l’amont s’engage à ne pas casser ses consommateurs. Un langage publié est le format d’échange documenté qui l’accompagne : un schéma OpenAPI, un contrat Avro ou Protobuf, un standard sectoriel.

L’intérêt est économique : sans eux, l’amont finit par maintenir une intégration sur mesure par consommateur. Le prix est le versionnement et la rétrocompatibilité, qui sont un travail permanent.

Chemins séparés

Aucune intégration. Chaque contexte résout son besoin de son côté, y compris si cela implique une duplication.

C’est le patron le plus sous-utilisé du catalogue. Le réflexe par défaut d’une équipe technique est d’intégrer ; or le coût d’une intégration — coordination, contrat, pannes, latence — dépasse fréquemment celui d’une petite redondance assumée. Si le besoin partagé est mince et que le couplage serait durable, ne rien faire est une décision d’architecture parfaitement défendable.

Grande boule de boue

Un constat, pas un objectif : une zone du système où les modèles sont indiscernables et le langage incohérent. Le geste utile est de la nommer et de l’entourer d’une frontière sur la carte, puis de la traiter comme une boîte noire derrière une couche d’anticorruption — plutôt que de tenter d’y appliquer des patrons tactiques, ce qui n’a jamais fonctionné.

Choisir

Deux questions suffisent à réduire le choix.

Tenez-vous à votre modèle ? Ce qui revient à demander : est-ce du cœur de métier ? Si oui, une couche d’anticorruption est presque toujours justifiée. Si non, le conformisme économise du travail.

Qui a le pouvoir ? Si vous pouvez négocier, visez client/fournisseur ou partenariat. Si vous ne pouvez pas — progiciel, prestataire, autre direction — vous n’avez le choix qu’entre conformiste, anticorruption et chemins séparés.

                        Tenez-vous à votre modèle ?
                          non              oui
                       ┌───────────────┬────────────────────┐
   Pouvez-vous     oui │  Conformiste  │  Client/Fournisseur │
   négocier ?          │  (économique) │  ou Partenariat     │
                   non │  Conformiste  │  Anticorruption     │
                       │  (subi)       │  ou Chemins séparés │
                       └───────────────┴────────────────────┘

Ce que la carte révèle vraiment

Une carte de contextes honnête est un document politique autant que technique, et c’est là son intérêt. Trois lectures valent l’effort de la dessiner :

  • Les points de fragilité. Un contexte cœur en position d’aval conformiste derrière un système que vous ne maîtrisez pas est un risque majeur, et la carte le rend visible en un coup d’œil.
  • La cohérence avec l’organisation. La loi de Conway n’est pas une malédiction mais un outil : si la carte souhaitée ne correspond pas aux équipes, il faut changer l’une ou les autres — et changer les équipes est souvent le levier le plus efficace.
  • Les noyaux partagés qui ont grossi. Ils apparaissent comme des relations symétriques entre cinq contextes, ce qui n’existe pas : c’est un modèle unifié déguisé.

Références

  • Eric Evans, Domain-Driven Design, chapitre 14 — Context Map
  • https://github.com/ddd-crew/context-mapping — notation visuelle et fiches par patron
  • Matthew Skelton & Manuel Pais, Team Topologies, 2019 — la contrepartie organisationnelle