A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 71KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang=fr>
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset=utf-8>
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Feature Toggles (aka Feature Flags) (archive) — David Larlet</title>
  13. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  14. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
  15. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
  16. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
  17. <link rel="manifest" href="/manifest.json">
  18. <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
  19. <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
  20. <meta name="apple-mobile-web-app-title" content="David Larlet">
  21. <meta name="application-name" content="David Larlet">
  22. <meta name="msapplication-TileColor" content="#da532c">
  23. <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
  24. <meta name="theme-color" content="#f0f0ea">
  25. <!-- That good ol' feed, subscribe :p. -->
  26. <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
  27. <meta name="robots" content="noindex, nofollow">
  28. <meta content="origin-when-cross-origin" name="referrer">
  29. <!-- Canonical URL for SEO purposes -->
  30. <link rel="canonical" href="https://martinfowler.com/articles/feature-toggles.html">
  31. <style>
  32. /* http://meyerweb.com/eric/tools/css/reset/ */
  33. html, body, div, span,
  34. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  35. a, abbr, address, big, cite, code,
  36. del, dfn, em, img, ins,
  37. small, strike, strong, tt, var,
  38. dl, dt, dd, ol, ul, li,
  39. fieldset, form, label, legend,
  40. table, caption, tbody, tfoot, thead, tr, th, td,
  41. article, aside, canvas, details, embed,
  42. figure, figcaption, footer, header, hgroup,
  43. menu, nav, output, ruby, section, summary,
  44. time, mark, audio, video {
  45. margin: 0;
  46. padding: 0;
  47. border: 0;
  48. font-size: 100%;
  49. font: inherit;
  50. vertical-align: baseline;
  51. }
  52. /* HTML5 display-role reset for older browsers */
  53. article, aside, details, figcaption, figure,
  54. footer, header, hgroup, menu, nav, section { display: block; }
  55. body { line-height: 1; }
  56. blockquote, q { quotes: none; }
  57. blockquote:before, blockquote:after,
  58. q:before, q:after {
  59. content: '';
  60. content: none;
  61. }
  62. table {
  63. border-collapse: collapse;
  64. border-spacing: 0;
  65. }
  66. /* http://practicaltypography.com/equity.html */
  67. /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
  68. /* https://www.filamentgroup.com/lab/js-web-fonts.html */
  69. @font-face {
  70. font-family: 'EquityTextB';
  71. src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
  72. url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
  73. font-weight: 300;
  74. font-style: normal;
  75. font-display: swap;
  76. }
  77. @font-face {
  78. font-family: 'EquityTextB';
  79. src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
  80. url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
  81. font-weight: 300;
  82. font-style: italic;
  83. font-display: swap;
  84. }
  85. @font-face {
  86. font-family: 'EquityTextB';
  87. src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
  88. url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
  89. font-weight: 700;
  90. font-style: normal;
  91. font-display: swap;
  92. }
  93. @font-face {
  94. font-family: 'ConcourseT3';
  95. src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
  96. url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
  97. font-weight: 300;
  98. font-style: normal;
  99. font-display: swap;
  100. }
  101. /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
  102. body {
  103. /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
  104. font-family: "EquityTextB", Palatino, serif;
  105. background-color: #f0f0ea;
  106. color: #07486c;
  107. font-kerning: normal;
  108. -moz-osx-font-smoothing: grayscale;
  109. -webkit-font-smoothing: subpixel-antialiased;
  110. text-rendering: optimizeLegibility;
  111. font-variant-ligatures: common-ligatures contextual;
  112. font-feature-settings: "kern", "liga", "clig", "calt";
  113. }
  114. pre, code, kbd, samp, var, tt {
  115. font-family: 'TriplicateT4c', monospace;
  116. }
  117. em {
  118. font-style: italic;
  119. color: #323a45;
  120. }
  121. strong {
  122. font-weight: bold;
  123. color: black;
  124. }
  125. nav {
  126. background-color: #323a45;
  127. color: #f0f0ea;
  128. display: flex;
  129. justify-content: space-around;
  130. padding: 1rem .5rem;
  131. }
  132. nav:last-child {
  133. border-bottom: 1vh solid #2d7474;
  134. }
  135. nav a {
  136. color: #f0f0ea;
  137. }
  138. nav abbr {
  139. border-bottom: 1px dotted white;
  140. }
  141. h1 {
  142. border-top: 1vh solid #2d7474;
  143. border-bottom: .2vh dotted #2d7474;
  144. background-color: #e3e1e1;
  145. color: #323a45;
  146. text-align: center;
  147. padding: 5rem 0 4rem 0;
  148. width: 100%;
  149. font-family: 'ConcourseT3';
  150. display: flex;
  151. flex-direction: column;
  152. }
  153. h1.single {
  154. padding-bottom: 10rem;
  155. }
  156. h1 span {
  157. position: absolute;
  158. top: 1vh;
  159. left: 20%;
  160. line-height: 0;
  161. }
  162. h1 span a {
  163. line-height: 1.7;
  164. padding: 1rem 1.2rem .6rem 1.2rem;
  165. border-radius: 0 0 6% 6%;
  166. background: #2d7474;
  167. font-size: 1.3rem;
  168. color: white;
  169. text-decoration: none;
  170. }
  171. h2 {
  172. margin: 4rem 0 1rem;
  173. border-top: .2vh solid #2d7474;
  174. padding-top: 1vh;
  175. }
  176. h3 {
  177. text-align: center;
  178. margin: 3rem 0 .75em;
  179. }
  180. hr {
  181. height: .4rem;
  182. width: .4rem;
  183. border-radius: .4rem;
  184. background: #07486c;
  185. margin: 2.5rem auto;
  186. }
  187. time {
  188. display: bloc;
  189. margin-left: 0 !important;
  190. }
  191. ul, ol {
  192. margin: 2rem;
  193. }
  194. ul {
  195. list-style-type: square;
  196. }
  197. a {
  198. text-decoration-skip-ink: auto;
  199. text-decoration-thickness: 0.05em;
  200. text-underline-offset: 0.09em;
  201. }
  202. article {
  203. max-width: 50rem;
  204. display: flex;
  205. flex-direction: column;
  206. margin: 2rem auto;
  207. }
  208. article.single {
  209. border-top: .2vh dotted #2d7474;
  210. margin: -6rem auto 1rem auto;
  211. background: #f0f0ea;
  212. padding: 2rem;
  213. }
  214. article p:last-child {
  215. margin-bottom: 1rem;
  216. }
  217. p {
  218. padding: 0 .5rem;
  219. margin-left: 3rem;
  220. }
  221. p + p,
  222. figure + p {
  223. margin-top: 2rem;
  224. }
  225. blockquote {
  226. background-color: #e3e1e1;
  227. border-left: .5vw solid #2d7474;
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. padding: 1rem;
  232. margin: 1.5rem;
  233. }
  234. blockquote cite {
  235. font-style: italic;
  236. }
  237. blockquote p {
  238. margin-left: 0;
  239. }
  240. figure {
  241. border-top: .2vh solid #2d7474;
  242. background-color: #e3e1e1;
  243. text-align: center;
  244. padding: 1.5rem 0;
  245. margin: 1rem 0 0;
  246. font-size: 1.5rem;
  247. width: 100%;
  248. }
  249. figure img {
  250. max-width: 250px;
  251. max-height: 250px;
  252. border: .5vw solid #323a45;
  253. padding: 1px;
  254. }
  255. figcaption {
  256. padding: 1rem;
  257. line-height: 1.4;
  258. }
  259. aside {
  260. display: flex;
  261. flex-direction: column;
  262. background-color: #e3e1e1;
  263. padding: 1rem 0;
  264. border-bottom: .2vh solid #07486c;
  265. }
  266. aside p {
  267. max-width: 50rem;
  268. margin: 0 auto;
  269. }
  270. /* https://fvsch.com/code/css-locks/ */
  271. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  272. font-size: 1rem;
  273. line-height: calc( 1.5em + 0.2 * 1rem );
  274. }
  275. h1 {
  276. font-size: 1.9rem;
  277. line-height: calc( 1.2em + 0.2 * 1rem );
  278. }
  279. h2 {
  280. font-size: 1.6rem;
  281. line-height: calc( 1.3em + 0.2 * 1rem );
  282. }
  283. h3 {
  284. font-size: 1.35rem;
  285. line-height: calc( 1.4em + 0.2 * 1rem );
  286. }
  287. @media (min-width: 20em) {
  288. /* The (100vw - 20rem) / (50 - 20) part
  289. resolves to 0-1rem, depending on the
  290. viewport width (between 20em and 50em). */
  291. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  292. font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
  293. line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
  294. margin-left: 0;
  295. }
  296. h1 {
  297. font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  298. line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
  299. }
  300. h2 {
  301. font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  302. line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
  303. }
  304. h3 {
  305. font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  306. line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
  307. }
  308. }
  309. @media (min-width: 50em) {
  310. /* The right part of the addition *must* be a
  311. rem value. In this example we *could* change
  312. the whole declaration to font-size:2.5rem,
  313. but if our baseline value was not expressed
  314. in rem we would have to use calc. */
  315. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  316. font-size: calc( 1rem + .6 * 1rem );
  317. line-height: 1.5em;
  318. }
  319. p, li, pre, details {
  320. margin-left: 3rem;
  321. }
  322. h1 {
  323. font-size: calc( 1.9rem + 1.5 * 1rem );
  324. line-height: 1.2em;
  325. }
  326. h2 {
  327. font-size: calc( 1.5rem + 1.5 * 1rem );
  328. line-height: 1.3em;
  329. }
  330. h3 {
  331. font-size: calc( 1.35rem + 1.5 * 1rem );
  332. line-height: 1.4em;
  333. }
  334. figure img {
  335. max-width: 500px;
  336. max-height: 500px;
  337. }
  338. }
  339. figure.unsquared {
  340. margin-bottom: 1.5rem;
  341. }
  342. figure.unsquared img {
  343. height: inherit;
  344. }
  345. @media print {
  346. body { font-size: 100%; }
  347. a:after { content: " (" attr(href) ")"; }
  348. a, a:link, a:visited, a:after {
  349. text-decoration: underline;
  350. text-shadow: none !important;
  351. background-image: none !important;
  352. background: white;
  353. color: black;
  354. }
  355. abbr[title] { border-bottom: 0; }
  356. abbr[title]:after { content: " (" attr(title) ")"; }
  357. img { page-break-inside: avoid; }
  358. @page { margin: 2cm .5cm; }
  359. h1, h2, h3 { page-break-after: avoid; }
  360. p3 { orphans: 3; widows: 3; }
  361. img {
  362. max-width: 250px !important;
  363. max-height: 250px !important;
  364. }
  365. nav, aside { display: none; }
  366. }
  367. ul.with_columns {
  368. column-count: 1;
  369. }
  370. @media (min-width: 20em) {
  371. ul.with_columns {
  372. column-count: 2;
  373. }
  374. }
  375. @media (min-width: 50em) {
  376. ul.with_columns {
  377. column-count: 3;
  378. }
  379. }
  380. ul.with_two_columns {
  381. column-count: 1;
  382. }
  383. @media (min-width: 20em) {
  384. ul.with_two_columns {
  385. column-count: 1;
  386. }
  387. }
  388. @media (min-width: 50em) {
  389. ul.with_two_columns {
  390. column-count: 2;
  391. }
  392. }
  393. .gallery {
  394. display: flex;
  395. flex-wrap: wrap;
  396. justify-content: space-around;
  397. }
  398. .gallery figure img {
  399. margin-left: 1rem;
  400. margin-right: 1rem;
  401. }
  402. .gallery figure figcaption {
  403. font-family: 'ConcourseT3'
  404. }
  405. footer {
  406. font-family: 'ConcourseT3';
  407. display: flex;
  408. flex-direction: column;
  409. border-top: 3px solid white;
  410. padding: 4rem 0;
  411. background-color: #07486c;
  412. color: white;
  413. }
  414. footer > * {
  415. max-width: 50rem;
  416. margin: 0 auto;
  417. }
  418. footer a {
  419. color: #f1c40f;
  420. }
  421. footer .avatar {
  422. width: 200px;
  423. height: 200px;
  424. border-radius: 50%;
  425. float: left;
  426. -webkit-shape-outside: circle();
  427. shape-outside: circle();
  428. margin-right: 2rem;
  429. padding: 2px 5px 5px 2px;
  430. background: white;
  431. border-left: 1px solid #f1c40f;
  432. border-top: 1px solid #f1c40f;
  433. border-right: 5px solid #f1c40f;
  434. border-bottom: 5px solid #f1c40f;
  435. }
  436. </style>
  437. <h1>
  438. <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
  439. Feature Toggles (aka Feature Flags) (archive)
  440. <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
  441. </h1>
  442. <section>
  443. <article>
  444. <h3><a href="https://martinfowler.com/articles/feature-toggles.html">Source originale du contenu</a></h3>
  445. <p>"Feature Toggling" is a set of patterns which can help a team to deliver new
  446. functionality to users rapidly but safely. In this article on Feature Toggling we'll
  447. start off with a short story showing some typical scenarios where Feature Toggles are
  448. helpful. Then we'll dig into the details, covering specific patterns and practices
  449. which will help a team succeed with Feature Toggles.</p>
  450. <p>Feature Toggles are also refered to as Feature Flags, Feature Bits, or Feature Flippers.
  451. These are all synonyms for the same set of techniques. Throughout this article I'll use
  452. feature toggles and feature flags interchangebly.</p>
  453. <div id="ATogglingTale"><hr class="topSection"/>
  454. <h2>A Toggling Tale</h2>
  455. <p>Picture the scene. You're on one of several teams working on a sophisticated town
  456. planning simulation game. Your team is responsible for the core simulation engine.
  457. You have been tasked with increasing the efficiency of the Spline Reticulation
  458. algorithm. You know this will require a fairly large overhaul of the implementation
  459. which will take several weeks. Meanwhile other members of your team will need to
  460. continue some ongoing work on related areas of the codebase. </p>
  461. <p>You want to avoid branching for this work if at all possible, based on previous
  462. painful experiences of merging long-lived branches in the past. Instead, you decide
  463. that the entire team will continue to work on trunk, but the developers working on
  464. the Spline Reticulation improvements will use a Feature Toggle to prevent their work
  465. from impacting the rest of the team or destabilizing the codebase.</p>
  466. <div id="TheBirthOfAFeatureFlag">
  467. <h3>The birth of a Feature Flag</h3>
  468. <p>Here's the first change introduced by the pair working on the algorithm:</p>
  469. <p class="code-label">before
  470. </p>
  471. <pre class="code"> function reticulateSplines(){
  472. // current implementation lives here
  473. }</pre>
  474. <p class="code-label">after
  475. </p>
  476. <pre class="code"> function reticulateSplines(){
  477. var useNewAlgorithm = false;
  478. // useNewAlgorithm = true; // UNCOMMENT IF YOU ARE WORKING ON THE NEW SR ALGORITHM
  479. if( useNewAlgorithm ){
  480. return enhancedSplineReticulation();
  481. }else{
  482. return oldFashionedSplineReticulation();
  483. }
  484. }
  485. function oldFashionedSplineReticulation(){
  486. // current implementation lives here
  487. }
  488. function enhancedSplineReticulation(){
  489. // TODO: implement better SR algorithm
  490. }</pre>
  491. <p>The pair have moved the current algorithm implementation into an
  492. <code>oldFashionedSplineReticulation</code> function, and turned
  493. <code>reticulateSplines</code> into a <b>Toggle Point</b>. Now if someone is
  494. working on the new algorithm they can enable the "use new Algorithm"
  495. <b>Feature</b> by uncommenting the <code>useNewAlgorithm = true</code>
  496. line.</p>
  497. </div>
  498. <div id="MakingAFlagDynamic">
  499. <h3>Making a flag dynamic</h3>
  500. <p>A few hours pass and the pair are ready to run their new algorithm through some
  501. of the simulation engine's integration tests. They also want to exercise the old
  502. algorithm in the same integration test run. They'll need to be able to enable or
  503. disable the Feature dynamically, which means it's time to move on from the clunky
  504. mechanism of commenting or uncommenting that <code>useNewAlgorithm = true</code>
  505. line:</p>
  506. <pre class="code">function reticulateSplines(){
  507. if( featureIsEnabled("use-new-SR-algorithm") ){
  508. return enhancedSplineReticulation();
  509. }else{
  510. return oldFashionedSplineReticulation();
  511. }
  512. }
  513. </pre>
  514. <p>We've now introduced a <code>featureIsEnabled</code> function, a <b>Toggle
  515. Router</b> which can be used to dynamically control which codepath is live.
  516. There are many ways to implement a Toggle Router, varying from a simple in-memory
  517. store to a highly sophisticated distributed system with a fancy UI. For now we'll
  518. start with a very simple system:</p>
  519. <pre class="code">function createToggleRouter(featureConfig){
  520. return {
  521. setFeature(featureName,isEnabled){
  522. featureConfig[featureName] = isEnabled;
  523. },
  524. featureIsEnabled(featureName){
  525. return featureConfig[featureName];
  526. }
  527. };
  528. }
  529. </pre>
  530. <p>We can create a new toggle router based on some default configuration - perhaps
  531. read in from a config file - but we can also dynamically toggle a feature on or
  532. off. This allows automated tests to verify both sides of a toggled feature:</p>
  533. <pre class="code">describe( 'spline reticulation', function(){
  534. let toggleRouter;
  535. let simulationEngine;
  536. beforeEach(function(){
  537. toggleRouter = createToggleRouter();
  538. simulationEngine = createSimulationEngine({toggleRouter:toggleRouter});
  539. });
  540. it('works correctly with old algorithm', function(){
  541. // Given
  542. toggleRouter.setFeature("use-new-SR-algorithm",false);
  543. // When
  544. const result = simulationEngine.doSomethingWhichInvolvesSplineReticulation();
  545. // Then
  546. verifySplineReticulation(result);
  547. });
  548. it('works correctly with new algorithm', function(){
  549. // Given
  550. toggleRouter.setFeature("use-new-SR-algorithm",true);
  551. // When
  552. const result = simulationEngine.doSomethingWhichInvolvesSplineReticulation();
  553. // Then
  554. verifySplineReticulation(result);
  555. });
  556. });
  557. </pre>
  558. </div>
  559. <div id="GettingReadyToRelease">
  560. <h3>Getting ready to release</h3>
  561. <p>More time passes and the team believe their new algorithm is feature-complete.
  562. To confirm this they have been modifying their higher-level automated tests so
  563. that they exercise the system both with the feature off and with it on. The team
  564. also wants to do some manual exploratory testing to ensure everything works as
  565. expected - Spline Reticulation is a critical part of the system's behavior, after
  566. all. </p>
  567. <p>To perform manual testing of a feature which hasn't yet been verified as ready
  568. for general use we need to be able to have the feature Off for our general user
  569. base in production but be able to turn it On for internal users. There are a lot
  570. of different approaches to achieve this goal:</p>
  571. <ul>
  572. <li>Have the Toggle Router make decisions based on a <b>Toggle Configuration</b>,
  573. and make that configuration environment-specific. Only turn the new feature on in a
  574. pre-production environment.</li>
  575. <li>Allow Toggle Configuration to be modified at runtime via some form of admin UI. Use
  576. that admin UI to turn the new feature on a test environment.</li>
  577. <li>Teach the Toggle Router how to make dynamic, per-request toggling decisions.
  578. These decisions take <b>Toggle Context</b> into account, for example by looking for a special cookie
  579. or HTTP header. Usually Toggle Context is used as a proxy for identifying the user making the request.</li>
  580. </ul>
  581. <p>(We'll be digging into these approaches in more detail later on, so don't worry if some
  582. of these concepts are new to you.)</p>
  583. <p class="clear"/>
  584. <p>The team decides to go with a per-request Toggle Router since it gives them a lot of
  585. flexibility. The team particularly appreciate that this will allow them to test their new algorithm without needing
  586. a separate testing environment. Instead they can simply turn the algorithm on in their
  587. production environment but only for internal users (as detected via a special cookie). The
  588. team can now turn that cookie on for themselves and verify that the new feature performs
  589. as expected.</p>
  590. </div>
  591. <div id="CanaryReleasing">
  592. <h3>Canary releasing</h3>
  593. <p>The new Spline Reticulation algorithm is looking good based on the exploratory
  594. testing done so far. However since it's such a critical part of the game's
  595. simulation engine there remains some reluctance to turn this feature on for all
  596. users. The team decide to use their Feature Flag infrastructure to perform a
  597. <a href="/bliki/CanaryRelease.html"><b>Canary Release</b></a>, only turning the new
  598. feature on for a small percentage of their total userbase - a "canary" cohort.
  599. </p>
  600. <p>The team enhance the Toggle Router by teaching it the concept of user cohorts -
  601. groups of users who consistently experience a feature as always being On or Off. A
  602. cohort of canary users is created via a random sampling of 1% of the user base -
  603. perhaps using a modulo of user ID. This canary cohort will consistently have the
  604. feature turned on, while the other 99% of the user base remain using the old
  605. algorithm. Key business metrics (user engagement, total revenue earned, etc) are
  606. monitored for both groups to gain confidence that the new algorithm does not
  607. negatively impact user behavior. Once the team are confident that the new feature has no
  608. ill effects they modify their Toggle Configuration to turn it on for the entire user
  609. base.</p>
  610. </div>
  611. <div id="AbTesting">
  612. <h3>A/B testing</h3>
  613. <p>The team's product manager learns about this approach and is quite excited. She
  614. suggests that the team use a similar mechanism to perform some A/B testing. There's been a
  615. long-running debate as to whether modifying the crime rate algorithm to take
  616. pollution levels into account would increase or decrease the game's playability.
  617. They now have the ability to settle the debate using data. They plan to roll out a
  618. cheap implementation which captures the essence of the idea, controlled with a
  619. Feature Flag. They will turn the feature on for a reasonably large cohort of
  620. users, then study how those users behave compared to a "control" cohort. This approach will allow
  621. the team to resolve contentious product debates based on data, rather than <a href="http://www.forbes.com/sites/derosetichy/2013/04/15/what-happens-when-a-hippo-runs-your-company/">HiPPOs</a>.</p>
  622. </div>
  623. <p>This brief scenario is intended to illustrate both the basic concept of Feature
  624. Toggling but also to highlight how many different applications this core capability
  625. can have. Now that we've seen some examples of those applications let's dig a little
  626. deeper. We'll explore different categories of toggles and see what makes them
  627. different. We'll cover how to write maintainable toggle code, and finally share
  628. practices to avoid some of pitfalls of a feature-toggled system.</p>
  629. </div>
  630. <div id="CategoriesOfToggles"><hr class="topSection"/>
  631. <h2>Categories of toggles</h2>
  632. <p>We've seen the fundamental facility provided by Feature Toggles - being able to
  633. ship alternative codepaths within one deployable unit and choose between them at
  634. runtime. The scenarios above also show that this facility can be used in various
  635. ways in various contexts. It can be tempting to lump all feature toggles into the
  636. same bucket, but this is a dangerous path. The design forces at play for different
  637. categories of toggles are quite different and managing them all in the same way can
  638. lead to pain down the road. </p>
  639. <p>Feature toggles can be categorized across two major dimensions: how long the
  640. feature toggle will live and how dynamic the toggling decision must be. There are
  641. other factors to consider - who will manage the feature toggle, for example - but I
  642. consider longevity and dynamism to be two big factors which can help guide how to manage
  643. toggles.</p>
  644. <p>Let's consider various categories of toggle through the lens of these two
  645. dimensions and see where they fit.</p>
  646. <div id="ReleaseToggles">
  647. <h3>Release Toggles</h3>
  648. <div class="soundbite">
  649. <p>
  650. Release Toggles allow incomplete and un-tested codepaths to be shipped to production as latent code which may never be turned on.
  651. </p>
  652. </div>
  653. <p>These are feature flags used to enable trunk-based development for teams practicing
  654. Continuous Delivery. They allow in-progress features to be checked into a shared
  655. integration branch (e.g. master or trunk) while still allowing that branch to be deployed to production at
  656. any time. Release Toggles allow incomplete and un-tested codepaths to be shipped to
  657. production as <a href="http://www.infoq.com/news/2009/08/enabling-lrm">latent code</a> which may
  658. never be turned on. </p>
  659. <p>Product Managers may also use a product-centric version of this same approach to
  660. prevent half-complete product features from being exposed to their end users. For
  661. example, the product manager of an ecommerce site might not want to let users see a
  662. new Estimated Shipping Date feature which only works for one of the site's shipping
  663. partners, preferring to wait until that feature has been implemented for all shipping
  664. partners. Product Managers may have other reasons for not wanting to expose features
  665. even if they are fully implemented and tested. Feature release might be being
  666. coordinated with a marketing campaign, for example. Using Release Toggles in this way
  667. is the most common way to implement the Continuous Delivery principle of "separating
  668. [feature] release from [code] deployment."</p>
  669. <div class="figure " id="chart-1.png"><img src="feature-toggles/chart-1.png"/>
  670. <p class="photoCaption"/>
  671. </div>
  672. <p class="clear"/>
  673. <p>Release Toggles are transitionary by nature. They should generally not stick around
  674. much longer than a week or two, although product-centric toggles may need to remain in
  675. place for a longer period. The toggling decision for a Release Toggle is
  676. typically very static. Every toggling decision for a given release version will be the
  677. same, and changing that toggling decision by rolling out a new release with a toggle
  678. configuration change is usually perfectly acceptable.</p>
  679. </div>
  680. <div id="ExperimentToggles">
  681. <h3>Experiment Toggles</h3>
  682. <p>Experiment Toggles are used to perform multivariate or A/B testing. Each user of
  683. the system is placed into a cohort and at runtime the Toggle Router will
  684. consistently send a given user down one codepath or the other, based upon which
  685. cohort they are in. By tracking the aggregate behavior of different cohorts we can
  686. compare the effect of different codepaths. This technique is commonly used
  687. to make data-driven optimizations to things such as the purchase flow of an
  688. ecommerce system, or the Call To Action wording on a button.</p>
  689. <div class="figure " id="chart-2.png"><img src="feature-toggles/chart-2.png"/>
  690. <p class="photoCaption"/>
  691. </div>
  692. <p class="clear"/>
  693. <p>An Experiment Toggle needs to remain in place with the same configuration long
  694. enough to generate statistically significant results. Depending on traffic patterns
  695. that might mean a lifetime of hours or weeks. Longer is unlikely to be useful, as
  696. other changes to the system risk invalidating the results of the experiment. By
  697. their nature Experiment Toggles are highly dynamic - each incoming request is likely
  698. on behalf of a different user and thus might be routed differently than the last.
  699. </p>
  700. </div>
  701. <div id="OpsToggles">
  702. <h3>Ops Toggles</h3>
  703. <p>These flags are used to control operational aspects of our system's behavior.
  704. We might introduce an Ops Toggle when rolling out a new feature which has unclear
  705. performance implications so that system operators can disable or degrade that
  706. feature quickly in production if needed. </p>
  707. <p>Most Ops Toggles will be relatively short-lived - once confidence is gained in
  708. the operational aspects of a new feature the flag should be retired. However it's
  709. not uncommon for systems to have a small number of long-lived "Kill Switches" which
  710. allow operators of production environments to gracefully degrade non-vital system
  711. functionality when the system is enduring unusually high load. For example, when
  712. we're under heavy load we might want to disable a Recommendations panel on our home
  713. page which is relatively expensive to generate. I consulted with an online retailer
  714. that maintained Ops Toggles which could intentionally disable many non-critical
  715. features in their website's main purchasing flow just prior to a high-demand product
  716. launch. These types of long-lived Ops Toggles could be seen as a manually-managed
  717. <a href="/bliki/CircuitBreaker.html">Circuit Breaker</a>.</p>
  718. <div class="figure " id="chart-3.png"><img src="feature-toggles/chart-3.png"/>
  719. <p class="photoCaption"/>
  720. </div>
  721. <p class="clear"/>
  722. <p>As already mentioned, many of these flags are only in place for a short while, but a few
  723. key controls may be left in place for operators almost indefinitely. Since the
  724. purpose of these flags is to allow operators to quickly react to production
  725. issues they need to be re-configured extremely quickly - needing to roll out a
  726. new release in order to flip an Ops Toggle is unlikely to make an Operations person happy.</p>
  727. </div>
  728. <div id="PermissioningToggles">
  729. <h3>Permissioning Toggles</h3>
  730. <div class="soundbite">
  731. <p>turning on new features for a set of internal users [is a] Champagne Brunch - an early opportunity to drink your own champagne</p>
  732. </div>
  733. <p>These flags are used to change the features or product experience that certain
  734. users receive. For example we may have a set of "premium" features which we only
  735. toggle on for our paying customers. Or perhaps we have a set of "alpha" features
  736. which are only available to internal users and another set of "beta" features which
  737. are only available to internal users plus beta users. I refer to this technique of
  738. turning on new features for a set of internal or beta users as a Champagne Brunch -
  739. an early opportunity to "<a href="http://www.cio.com/article/122351/Pegasystems_CIO_Tells_Colleagues_Drink_Your_Own_Champagne">drink your own
  740. champagne</a>".
  741. </p>
  742. <p>A Champagne Brunch is similar in many ways to a Canary Release. The distinction
  743. between the two is that a Canary Released feature is exposed to a randomly selected
  744. cohort of users while a Champagne Brunch feature is exposed to a specific set of
  745. users.</p>
  746. <p class="clear"/>
  747. <p>When used as a way to manage a feature which is only exposed to premium users a
  748. Permissioning Toggle may be very-long lived compared to other categories of Feature
  749. Toggles - at the scale of multiple years. Since permissions are user-specific the toggling
  750. decision for a Permissioning Toggle will always be per-request, making this a very dynamic toggle.</p>
  751. </div>
  752. <div id="ManagingDifferentCategoriesOfToggles">
  753. <h3>Managing different categories of toggles</h3>
  754. <p>Now that we have a toggle categorization scheme we can discuss how those two
  755. dimensions of dynamism and longevity affect how we work with feature flags of different
  756. categories.</p>
  757. <div id="StaticVsDynamicToggles">
  758. <h4>static vs dynamic toggles</h4>
  759. <p class="clear"/>
  760. <p>Toggles which are making runtime routing decisions necessarily need more
  761. sophisticated Toggle Routers, along with more complex configuration for those
  762. routers.</p>
  763. <p>For simple static routing decisions a toggle configuration can be a simple On
  764. or Off for each feature with a toggle router which is just responsible for relaying that
  765. static on/off state to the Toggle Point. As we discussed earlier, other
  766. categories of toggle are more dynamic and demand more sophisticated toggle
  767. routers. For example the router for an Experiment Toggle makes routing
  768. decisions dynamically for a given user, perhaps using some sort of consistent
  769. cohorting algorithm based on that user's id. Rather than reading a static toggle
  770. state from configuration this toggle router will instead need to read some sort of
  771. cohort configuration defining things like how large the experimental cohort and
  772. control cohort should be. That configuration would be used as an input into the
  773. cohorting algorithm. </p>
  774. <p>We'll dig into more detail on different ways to manage this toggle
  775. configuration later on.</p>
  776. </div>
  777. <div id="Long-livedTogglesVsTransientToggles">
  778. <h4>Long-lived toggles vs transient toggles</h4>
  779. <p class="clear"/>
  780. <p>We can also divide our toggle categories into those which are essentially
  781. transient in nature vs. those which are long-lived and may be in place for years.
  782. This distinction should have a strong influence on our approach to implementing
  783. a feature's Toggle Points. If
  784. we're adding a Release Toggle which will be removed in a few days time then we can
  785. probably get away with a Toggle Point which does a simple if/else check on a
  786. Toggle Router. This is what we did with our spline reticulation example
  787. earlier:</p>
  788. <pre class="code">function reticulateSplines(){
  789. if( featureIsEnabled("use-new-SR-algorithm") ){
  790. return enhancedSplineReticulation();
  791. }else{
  792. return oldFashionedSplineReticulation();
  793. }
  794. }
  795. </pre>
  796. <p>However if we're creating a new Permissioning Toggle with Toggle Points which
  797. we expect to stick around for a very long time then we certainly don't want to
  798. implement those Toggle Points by sprinkling if/else checks around
  799. indiscriminately. We'll need to use more maintainable implementation
  800. techniques.</p>
  801. </div>
  802. </div>
  803. </div>
  804. <div id="ImplementationTechniques"><hr class="topSection"/>
  805. <h2>Implementation Techniques</h2>
  806. <p>Feature Flags seem to beget rather messy Toggle Point code, and these Toggle
  807. Points also have a tendency to proliferate throughout a codebase. It's important to
  808. keep this tendency in check for any feature flags in your codebase, and critically
  809. important if the flag will be long-lived. There are a few implementation patterns
  810. and practices which help to reduce this issue.</p>
  811. <div id="De-couplingDecisionPointsFromDecisionLogic">
  812. <h3>De-coupling decision points from decision logic</h3>
  813. <p>One common mistake with Feature Toggles is to couple the place where a toggling
  814. decision is made (the Toggle Point) with the logic behind the decision (the Toggle
  815. Router). Let's look at an example. We're working on the next generation of our
  816. ecommerce system. One of our new features will allow a user to easily cancel an
  817. order by clicking a link inside their order confirmation email (aka invoice email). We're using
  818. feature flags to manage the rollout of all our next gen functionality. Our
  819. initial feature flagging implementation looks like this:</p>
  820. <p class="code-label">invoiceEmailer.js
  821. </p>
  822. <pre class="code"> const features = fetchFeatureTogglesFromSomewhere();
  823. function generateInvoiceEmail(){
  824. const baseEmail = buildEmailForInvoice(this.invoice);
  825. if( features.isEnabled("next-gen-ecomm") ){
  826. return addOrderCancellationContentToEmail(baseEmail);
  827. }else{
  828. return baseEmail;
  829. }
  830. }
  831. </pre>
  832. <p>While generating the invoice email our
  833. InvoiceEmailler checks to see whether the <code>next-gen-ecomm</code> feature is enabled. If
  834. it is then the emailer adds some extra order cancellation content to the
  835. email.</p>
  836. <p>While this looks like a reasonable approach, it's very brittle. The decision on
  837. whether to include order cancellation functionality in our invoice emails is wired
  838. directly to that rather broad <code>next-gen-ecomm</code> feature - using a magic string, no less. Why should
  839. the invoice emailling code need to know that the order cancellation content is
  840. part of the next-gen feature set? What happens if we'd like to turn on some parts
  841. of the next-gen functionality without exposing order cancellation? Or vice versa?
  842. What if we decide we'd like to only roll out order cancellation to certain users?
  843. It is quite common for these sort of "toggle scope" changes to occur as features
  844. are developed. Also bear in mind that these toggle points tend to proliferate
  845. throughout a codebase. With our current approach since the toggling decision logic
  846. is part of the toggle point any change to that decision logic will require
  847. trawling through all those toggle points which have spread through the
  848. codebase.</p>
  849. <p>Happily, <a href="https://en.wikipedia.org/wiki/Fundamental_theorem_of_software_engineering">any
  850. problem in software can be solved by adding a layer of indirection</a>. We can
  851. decouple a toggling decision point from the logic behind that decision like
  852. so:</p>
  853. <p class="code-label">featureDecisions.js
  854. </p>
  855. <pre class="code"> function createFeatureDecisions(features){
  856. return {
  857. includeOrderCancellationInEmail(){
  858. return features.isEnabled("next-gen-ecomm");
  859. }
  860. // ... additional decision functions also live here ...
  861. };
  862. }
  863. </pre>
  864. <p class="code-label">invoiceEmailer.js
  865. </p>
  866. <pre class="code"> const features = fetchFeatureTogglesFromSomewhere();
  867. const featureDecisions = createFeatureDecisions(features);
  868. function generateInvoiceEmail(){
  869. const baseEmail = buildEmailForInvoice(this.invoice);
  870. if( featureDecisions.includeOrderCancellationInEmail() ){
  871. return addOrderCancellationContentToEmail(baseEmail);
  872. }else{
  873. return baseEmail;
  874. }
  875. }
  876. </pre>
  877. <p>We've introduced a <code>FeatureDecisions</code> object, which acts as a collection point
  878. for any feature toggle decision logic. We create a decision method on this object
  879. for each specific toggling decision in our code - in this case "should we include
  880. order cancellation functionality in our invoice email" is represented by the
  881. <code>includeOrderCancellationInEmail</code> decision method. Right now the decision "logic"
  882. is a trivial pass-through to check the state of the <code>next-gen-ecomm</code> feature, but
  883. now as that logic evolves we have a singular place to manage it. Whenever we want
  884. to modify the logic of that specific toggling decision we have a single place to
  885. go. We might want to modify the scope of the decision - for example which specific
  886. feature flag controls the decision. Alternatively we might need to modify the
  887. reason for the decision - from being driven by a static toggle configuration to being
  888. driven by an A/B experiment, or by an operational concern such as an outage in
  889. some of our order cancellation infrastructure. In all cases our invoice emailer
  890. can remain blissfully unaware of how or why that toggling decision is being
  891. made.</p>
  892. </div>
  893. <div id="InversionOfDecision">
  894. <h3>Inversion of Decision</h3>
  895. <p>In the previous example our invoice emailer was responsible for asking the
  896. feature flagging infrastructure how it should perform. This means our invoice emailer has one
  897. extra concept it needs to be aware of - feature flagging - and an extra module it
  898. is coupled to. This makes the invoice emailer harder to work with and think about
  899. in isolation, including making it harder to test. As feature flagging has a
  900. tendency to become more and more prevalent in a system over time we will see more
  901. and more modules becoming coupled to the feature flagging system as a global
  902. dependency. Not the ideal scenario.</p>
  903. <p>In software design we can often solve these coupling issues by applying
  904. Inversion of Control. This is true in this case. Here's how we might decouple our
  905. invoice emailer from our feature flagging infrastructure:</p>
  906. <p class="code-label">invoiceEmailer.js
  907. </p>
  908. <pre class="code"> function createInvoiceEmailler(config){
  909. return {
  910. generateInvoiceEmail(){
  911. const baseEmail = buildEmailForInvoice(this.invoice);
  912. if( config.includeOrderCancellationInEmail ){
  913. return addOrderCancellationContentToEmail(email);
  914. }else{
  915. return baseEmail;
  916. }
  917. },
  918. // ... other invoice emailer methods ...
  919. };
  920. }</pre>
  921. <p class="code-label">featureAwareFactory.js
  922. </p>
  923. <pre class="code"> function createFeatureAwareFactoryBasedOn(featureDecisions){
  924. return {
  925. invoiceEmailler(){
  926. return createInvoiceEmailler({
  927. includeOrderCancellationInEmail: featureDecisions.includeOrderCancellationInEmail()
  928. });
  929. },
  930. // ... other factory methods ...
  931. };
  932. }</pre>
  933. <p>Now, rather than our <code>InvoiceEmailler</code> reaching out to <code>FeatureDecisions</code> it
  934. has those decisions injected into it at construction time via a <code>config</code> object.
  935. <code>InvoiceEmailler</code> now has no knowledge whatsoever about feature flagging. It just
  936. knows that some aspects of its behavior can be configured at runtime. This also
  937. makes testing <code>InvoiceEmailler</code>'s behavior easier - we can test the way that it
  938. generates emails both with and without order cancellation content just by passing
  939. a different configuration option during test:</p>
  940. <pre class="code">describe( 'invoice emailling', function(){
  941. it( 'includes order cancellation content when configured to do so', function(){
  942. // Given
  943. const emailler = createInvoiceEmailler({includeOrderCancellationInEmail:true});
  944. // When
  945. const email = emailler.generateInvoiceEmail();
  946. // Then
  947. verifyEmailContainsOrderCancellationContent(email);
  948. };
  949. it( 'does not includes order cancellation content when configured to not do so', function(){
  950. // Given
  951. const emailler = createInvoiceEmailler({includeOrderCancellationInEmail:false});
  952. // When
  953. const email = emailler.generateInvoiceEmail();
  954. // Then
  955. verifyEmailDoesNotContainOrderCancellationContent(email);
  956. };
  957. });
  958. </pre>
  959. <p>We also introduced a <code>FeatureAwareFactory</code> to centralize the creation of these
  960. decision-injected objects. This is an application of the general Dependency
  961. Injection pattern. If a DI system were in play in our codebase then we'd probably
  962. use that system to implement this approach.</p>
  963. </div>
  964. <div id="AvoidingConditionals">
  965. <h3>Avoiding conditionals</h3>
  966. <p>In our examples so far our Toggle Point has been implemented using an if
  967. statement. This might make sense for a simple, short-lived toggle. However point
  968. conditionals are not advised anywhere where a feature will require several Toggle Points, or
  969. where you expect the Toggle Point to be long-lived. A more maintainable
  970. alternative is to implement alternative codepaths using some sort of Strategy
  971. pattern:</p>
  972. <p class="code-label">invoiceEmailler.js
  973. </p>
  974. <pre class="code"> function createInvoiceEmailler(additionalContentEnhancer){
  975. return {
  976. generateInvoiceEmail(){
  977. const baseEmail = buildEmailForInvoice(this.invoice);
  978. return additionalContentEnhancer(baseEmail);
  979. },
  980. // ... other invoice emailer methods ...
  981. };
  982. }</pre>
  983. <p class="code-label">featureAwareFactory.js
  984. </p>
  985. <pre class="code"> function identityFn(x){ return x; }
  986. function createFeatureAwareFactoryBasedOn(featureDecisions){
  987. return {
  988. invoiceEmailler(){
  989. if( featureDecisions.includeOrderCancellationInEmail() ){
  990. return createInvoiceEmailler(addOrderCancellationContentToEmail);
  991. }else{
  992. return createInvoiceEmailler(identityFn);
  993. }
  994. },
  995. // ... other factory methods ...
  996. };
  997. }</pre>
  998. <p>Here we're applying a Strategy pattern by allowing our invoice emailer to be
  999. configured with a content enhancement function. <code>FeatureAwareFactory</code> selects a
  1000. strategy when creating the invoice emailer, guided by its <code>FeatureDecision</code>. If
  1001. order cancellation should be in the email it passes in an enhancer function which
  1002. adds that content to the email. Otherwise it passes in an <code>identityFn</code> enhancer -
  1003. one which has no effect and simply passes the email back without
  1004. modifications.</p>
  1005. </div>
  1006. </div>
  1007. <div id="ToggleConfiguration"><hr class="topSection"/>
  1008. <h2>Toggle Configuration</h2>
  1009. <div id="DynamicRoutingVsDynamicConfiguration">
  1010. <h3>Dynamic routing vs dynamic configuration</h3>
  1011. <p>Earlier we divided feature flags into those whose toggle routing decisions are
  1012. essentially static for a given code deployment vs those whose decisions vary
  1013. dynamically at runtime. It's important to note that there are two ways in which a
  1014. flag's decisions might change at runtime. Firstly, something like a
  1015. Ops Toggle might be dynamically <i>re-configured</i> from On to Off in response to a
  1016. system outage. Secondly, some categories of toggles such as Permissioning Toggles
  1017. and Experiment Toggles make a dynamic routing decision for each request based on
  1018. some request context such as which user is making the request. The former is
  1019. dynamic via re-configuration, while the later is inherently dynamic. These
  1020. inherently dynamic toggles may make highly dynamic <b>decisions</b> but still have a
  1021. <b>configuration</b> which is quite static, perhaps only changeable via
  1022. re-deployment. Experiment Toggles are an example of this type of feature flag - we
  1023. don't really need to be able to modify the parameters of an experiment at runtime.
  1024. In fact doing so would likely make the experiment statistically invalid.</p>
  1025. </div>
  1026. <div id="PreferStaticConfiguration">
  1027. <h3>Prefer static configuration</h3>
  1028. <p>Managing toggle configuration via source control and re-deployments is
  1029. preferable, if the nature of the feature flag allows it. Managing toggle configuration
  1030. via source control gives us the same benefits that we get by using source control
  1031. for things like infrastructure as code. It can allows toggle configuration
  1032. to live alongside the codebase being toggled, which provides a really big win:
  1033. toggle configuration will move through your Continuous Delivery pipeline in the
  1034. exact same way as a code change or an infrastructure change would. This enables
  1035. the full the benefits of CD - repeatable builds which are verified in a consistent
  1036. way across environments. It also greatly reduces the testing burden of feature flags.
  1037. There is less need to verify how the release will perform with both a toggle Off
  1038. and On, since that state is baked into the release and won't be changed (for less
  1039. dynamic flags at least). Another benefit of toggle configuration living
  1040. side-by-side in source control is that we can easily see the state of the toggle
  1041. in previous releases, and easily recreate previous releases if needed.</p>
  1042. </div>
  1043. <div id="ApproachesForManagingToggleConfiguration">
  1044. <h3>Approaches for managing toggle configuration</h3>
  1045. <p>While static configuration is preferable there are cases such as Ops Toggles where a more dynamic approach is required. Let's look at some options for managing toggle configuration, ranging from approaches which are simple but less dynamic
  1046. through to some approaches which are highly sophisticated but come with a lot of
  1047. additional complexity.</p>
  1048. </div>
  1049. <div id="HardcodedToggleConfiguration">
  1050. <h3>Hardcoded Toggle Configuration</h3>
  1051. <p>The most basic technique - perhaps so basic as to not be considered a Feature
  1052. Flag - is to simply comment or uncomment blocks of code. For example:</p>
  1053. <pre class="code">function reticulateSplines(){
  1054. //return oldFashionedSplineReticulation();
  1055. return enhancedSplineReticulation();
  1056. }
  1057. </pre>
  1058. <p>Slightly more sophisticated than the commenting approach is the use of a
  1059. preprocessor's <code>#ifdef</code> feature, where available.</p>
  1060. <p>Because this type of hardcoding doesn't allow dynamic re-configuration of a
  1061. toggle it is only suitable for feature flags where we're willing to follow a pattern of
  1062. deploying code in order to re-configure the flag.</p>
  1063. </div>
  1064. <div id="ParameterizedToggleConfiguration">
  1065. <h3>Parameterized Toggle Configuration</h3>
  1066. <p>The build-time configuration provided by hardcoded configuration isn't flexible
  1067. enough for many use cases, including a lot of testing scenarios. A simple approach which at least allows
  1068. feature flags to be re-configured without re-building an app or service is to specify
  1069. Toggle Configuration via command-line arguments or environment variables. This is
  1070. a simple and time-honored approach to toggling which has been around since well
  1071. before anyone referred to the technique as Feature Toggling or Feature Flagging. However it comes with
  1072. limitations. It can become unwieldy to coordinate configuration across a large
  1073. number of processes, and changes to a toggle's configuration require either a re-deploy or at the
  1074. very least a process restart (and probably privileged access to servers by the
  1075. person re-configuring the toggle too).</p>
  1076. </div>
  1077. <div id="ToggleConfigurationFile">
  1078. <h3>Toggle Configuration File</h3>
  1079. <p>Another option is to read Toggle Configuration from some sort of structured
  1080. file. It's quite common for this approach to Toggle Configuration to begin life as
  1081. one part of a more general application configuration file.</p>
  1082. <p>With a Toggle Configuration file you can now re-configure a feature flag by simply
  1083. changing that file rather than re-building application code itself. However,
  1084. although you don't need to re-build your app to toggle a feature in most cases
  1085. you'll probably still need to perform a re-deploy in order to re-configure a
  1086. flag.</p>
  1087. </div>
  1088. <div id="ToggleConfigurationInAppDb">
  1089. <h3>Toggle Configuration in App DB</h3>
  1090. <p>Using static files to manage toggle configuration can become cumbersome once
  1091. you reach a certain scale. Modifying configuration via files is relatively fiddly.
  1092. Ensuring consistency across a fleet of servers becomes a challenge, making changes
  1093. consistently even more so. In response to this many organizations move Toggle
  1094. Configuration into some type of centralized store, often an existing application
  1095. DB. This is usually accompanied by the build-out of some form of admin UI which
  1096. allows system operators, testers and product managers to view and modify Features
  1097. Flags and their configuration. </p>
  1098. </div>
  1099. <div id="DistributedToggleConfiguration">
  1100. <h3>Distributed Toggle Configuration</h3>
  1101. <p>Using a general purpose DB which is already part of the system architecture to
  1102. store toggle configuration is very common; it's an obvious place to go once
  1103. Feature Flags are introduced and start to gain traction. However nowadays there
  1104. are a breed of special-purpose hierarchical key-value stores which are a better
  1105. fit for managing application configuration - services like Zookeeper, etcd, or
  1106. Consul. These services form a distributed cluster which provides a shared source
  1107. of environmental configuration for all nodes attached to the cluster.
  1108. Configuration can be modified dynamically whenever required, and all nodes in the
  1109. cluster are automatically informed of the change - a very handy bonus feature.
  1110. Managing Toggle Configuration using these systems means we can have Toggle Routers
  1111. on each and every node in a fleet making decisions based on Toggle Configuration
  1112. which is coordinated across the entire fleet. </p>
  1113. <p>Some of these systems (such as Consul) come with an admin UI which provides a
  1114. basic way to manage Toggle Configuration. However at some point a small custom app
  1115. for administering toggle config is usually created.</p>
  1116. </div>
  1117. <div id="OverridingConfiguration">
  1118. <h3>Overriding configuration</h3>
  1119. <p>So far our discussion has assumed that all configuration is provided by a
  1120. singular mechanism. The reality for many systems is more sophisticated, with
  1121. overriding layers of configuration coming from various sources. With Toggle
  1122. Configuration it's quite common to have a default configuration along with
  1123. environment-specific overrides. Those overrides may come from something as simple
  1124. as an additional configuration file or something sophisticated like a Zookeeper
  1125. cluster. Be aware that any environment-specific overriding runs counter to the
  1126. Continuous Delivery ideal of having the exact same bits and configuration flow all
  1127. the way through your delivery pipeline. Often pragmatism dictates that some
  1128. environment-specific overrides are used, but striving to keep both your deployable
  1129. units and your configuration as environment-agnostic as possible will lead to a
  1130. simpler, safer pipeline. We'll re-visit this topic shortly when we talk about
  1131. testing a feature toggled system.</p>
  1132. <div id="Per-requestOverrides">
  1133. <h4>Per-request overrides</h4>
  1134. <p>An alternative approach to a environment-specific configuration overrides is
  1135. to allow a toggle's On/Off state to be overridden on a per-request basis by way
  1136. of a special cookie, query parameter, or HTTP header. This has a few advantages
  1137. over a full configuration override. If a service is load-balanced you can still
  1138. be confident that the override will be applied no matter which service instance
  1139. you are hitting. You can also override feature flags in a production environment
  1140. without affecting other users, and you're less likely to accidentally leave an
  1141. override in place. If the per-request override mechanism uses persistent cookies
  1142. then someone testing your system can configure their own custom set of toggle
  1143. overrides which will remain consistently applied in their browser. </p>
  1144. <p>The downside of this per-request approach is that it introduces a risk that
  1145. curious or malicious end-users may modify feature toggle state themselves. Some
  1146. organizations may be uncomfortable with the idea that some unreleased features
  1147. may be publicly accessible to a sufficiently determined party.
  1148. Cryptographically signing your override configuration is one option to alleviate
  1149. this concern, but regardless this approach will increase the complexity - and
  1150. attack surface - of your feature toggling system.</p>
  1151. <p>I elaborate on this technique for cookie-based overrides in <a href="http://blog.thepete.net/blog/2012/11/06/cookie-based-feature-flag-overrides/">this
  1152. post</a> and have also <a href="http://blog.thepete.net/blog/2013/08/24/introducing-rack-flags/">described a
  1153. ruby implementation</a> open-sourced by myself and a ThoughtWorks colleague.</p>
  1154. </div>
  1155. </div>
  1156. </div>
  1157. <div id="WorkingWithFeature-flaggedSystems"><hr class="topSection"/>
  1158. <h2>Working with feature-flagged systems </h2>
  1159. <p>While feature toggling is absolutely a helpful technique it does also bring
  1160. additional complexity. There are a few techniques which can help make life easier
  1161. when working with a feature-flagged system.</p>
  1162. <div id="ExposeCurrentFeatureToggleConfiguration">
  1163. <h3>Expose current feature toggle configuration</h3>
  1164. <p>It's always been a helpful practice to embed build/version numbers into a
  1165. deployed artifact and expose that metadata somewhere so that a dev, tester or operator can
  1166. find out what specific code is running in a given environment. The same idea
  1167. should be applied with feature flags. Any system using feature flags should
  1168. expose some way for an operator to discover the current state of the toggle
  1169. configuration. In an HTTP-oriented SOA system this is often accomplished via
  1170. some sort of metadata API endpoint or endpoints. See for example Spring Boot's
  1171. <a href="http://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html">Actuator
  1172. endpoints</a>.</p>
  1173. </div>
  1174. <div id="TakeAdvantageOfStructuredToggleConfigurationFiles">
  1175. <h3>Take advantage of structured Toggle Configuration files</h3>
  1176. <p>It's typical to store base Toggle Configuration in some sort of structured,
  1177. human-readable file (often in YAML format) managed via source-control. There are
  1178. some additional benefits we can derive from this file. Including a
  1179. human-readable description for each toggle is surprisingly useful, particularly
  1180. for toggles managed by folks other than the core delivery team. What would you
  1181. prefer to see when trying to decide whether to enable an Ops toggle
  1182. during a production outage event: <b>basic-rec-algo</b> or <b>"Use a simplistic
  1183. recommendation algorithm. This is fast and produces less load on backend
  1184. systems, but is way less accurate than our standard algorithm."</b>? Some teams also
  1185. opt to include additional metadata in their toggle configuration files such as a
  1186. creation date, a primary developer contact, or even an expiration date for
  1187. toggles which are intended to be short lived.</p>
  1188. </div>
  1189. <div id="ManageDifferentTogglesDifferently">
  1190. <h3>Manage different toggles differently</h3>
  1191. <p>As discussed earlier, there are various categories of Feature Toggles with
  1192. different characteristics. These differences should be embraced, and different
  1193. toggles managed in different ways, even if all the various toggles might
  1194. be controlled using the same technical machinery. </p>
  1195. <p>Let's revisit our previous example of an ecommerce site which has a
  1196. Recommended Products section on the homepage. Initially we might have placed
  1197. that section behind a Release Toggle while it was under development. We might
  1198. then have moved it to being behind an Experiment Toggle to validate that it was
  1199. helping drive revenue. Finally we might move it behind an Ops Toggle so that we
  1200. can turn it off when we're under extreme load. If we've followed the earlier
  1201. advice around de-coupling decision logic from Toggle Points then these
  1202. differences in toggle category should have had no impact on the Toggle Point
  1203. code at all. </p>
  1204. <p>However from a feature flag management perspective these transitions
  1205. absolutely should have an impact. As part of transitioning from Release Toggle
  1206. to an Experiment Toggle the way the toggle is configured will change, and likely
  1207. move to a different area - perhaps into an Admin UI rather than a yaml file in
  1208. source control. Product folks will likely now manage the configuration rather
  1209. than developers. Likewise, the transition from Experiment Toggle to Ops Toggle
  1210. will mean another change in how the toggle is configured, where that
  1211. configuration lives, and who manages the configuration.</p>
  1212. </div>
  1213. <div id="FeatureTogglesIntroduceValidationComplexity">
  1214. <h3>Feature Toggles introduce validation complexity</h3>
  1215. <p>With feature-flagged systems our Continuous Delivery process becomes more
  1216. complex, particularly in regard to testing. We'll often need to test
  1217. multiple codepaths for the same artifact as it moves through a CD pipeline. To
  1218. illustrate why, imagine we are shipping a system which can either use a new
  1219. optimized tax calculation algorithm if a toggle is on, or otherwise continue to
  1220. use our existing algorithm. At the time that a given deployable artifact is
  1221. moving through our CD pipeline we can't know whether the toggle will at some
  1222. point be turned On or Off in production - that's the whole point of feature
  1223. flags after all. Therefore in order to validate all codepaths which may end up
  1224. live in production we must perform test our artifact in <b>both</b> states: with
  1225. the toggle flipped On and flipped Off. </p>
  1226. <div class="figure " id="feature-toggles-testing.png"><img src="feature-toggles/feature-toggles-testing.png"/>
  1227. <p class="photoCaption"/>
  1228. </div>
  1229. <p class="clear"/>
  1230. <p>We can see that with a single toggle in play this introduces a requirement to
  1231. double up on at least some of our testing. With multiple toggles in play we have
  1232. a combinatoric explosion of possible toggle states. Validating behavior for each
  1233. of these states would be a monumental task. This can lead to some healthy
  1234. skepticism towards Feature Flags from folks with a testing focus. </p>
  1235. <p>Happily, the situation isn't as bad as some testers might initially imagine.
  1236. While a feature-flagged release candidate does need testing with a few toggle
  1237. configurations, it is not necessary to test *every* possible combination. Most
  1238. feature flags will not interact with each other, and most releases will not
  1239. involve a change to the configuration of more than one feature flag. </p>
  1240. <div class="soundbite">
  1241. <p>a good convention is to enable existing or legacy behavior when a Feature Flag is Off and new or future behavior when it's On.</p>
  1242. </div>
  1243. <p>So, which feature toggle configurations should a team test? It's most
  1244. important to test the toggle configuration which you expect to become live in
  1245. production, which means the current production toggle configuration plus any
  1246. toggles which you intend to release flipped On. It's also wise to test the
  1247. fall-back configuration where those toggles you intend to release are also
  1248. flipped Off. To avoid any surprise regressions in a future release many teams
  1249. also perform some tests with all toggles flipped On. Note that this advice only
  1250. makes sense if you stick to a convention of toggle semantics where existing or
  1251. legacy behavior is enabled when a feature is Off and new or future behavior is
  1252. enabled when a feature is On.</p>
  1253. <p>If your feature flag system doesn't support runtime configuration then you
  1254. may have to restart the process you're testing in order to flip a toggle, or
  1255. worse re-deploy an artifact into a testing environment. This can have a very
  1256. detrimental effect on the cycle time of your validation process, which in turn
  1257. impacts the all important feedback loop that CI/CD provides. To avoid this issue
  1258. consider exposing an endpoint which allows for dynamic in-memory
  1259. re-configuration of a feature flag. These types of override becomes even more
  1260. necessary when you are using things like Experiment Toggles where it's even more
  1261. fiddly to exercise both paths of a toggle.</p>
  1262. <p>This ability to dynamically re-configure specific service instances is a very
  1263. sharp tool. If used inappropriately it can cause a lot of pain and confusion
  1264. in a shared environment. This facility should only ever be used by automated
  1265. tests, and possibly as part of manual exploratory testing and debugging. If
  1266. there is a need for a more general-purpose toggle control mechanism for use in
  1267. production environments it would be best built out using a real distributed
  1268. configuration system as discussed in the Toggle Configuration section above.</p>
  1269. </div>
  1270. <div id="WhereToPlaceYourToggle">
  1271. <h3>Where to place your toggle</h3>
  1272. <div id="TogglesAtTheEdge">
  1273. <h4>Toggles at the edge</h4>
  1274. <p>For categories of toggle which need per-request context (Experiment
  1275. Toggles, Permissioning Toggles) it makes sense to place Toggle Points in the
  1276. edge services of your system - i.e. the publicly exposed web apps that present
  1277. functionality to end users. This is where your user's individual requests
  1278. first enter your domain and thus where your Toggle Router has the most context
  1279. available to make toggling decisions based on the user and their request. A
  1280. side-benefit of placing Toggle Points at the edge of your system is that it
  1281. keeps fiddly conditional toggling logic out of the core of your system. In
  1282. many cases you can place your Toggle Point right where you're rendering HTML,
  1283. as in this Rails example:</p>
  1284. <p class="code-label">someFile.erb
  1285. </p>
  1286. <pre class="code"> &lt;%= if featureDecisions.showRecommendationsSection? %&gt;
  1287. &lt;%= render 'recommendations_section' %&gt;
  1288. &lt;% end %&gt;</pre>
  1289. <p>Placing Toggle Points at the edges also makes sense when you are controlling access
  1290. to new user-facing features which aren't yet ready for launch. In this context you can
  1291. again control access using a toggle which simply shows or hides UI elements. As an
  1292. example, perhaps you are building the ability to <a href="https://developers.facebook.com/docs/facebook-login">log in to your application using
  1293. Facebook</a> but aren't ready to roll it out to users just yet. The implementation of
  1294. this feature may involve changes in various parts of your architecture, but you can
  1295. control exposure of the feature with a simple feature toggle at the UI layer which
  1296. hides the "Log in with Facebook" button.</p>
  1297. <p>It's interesting to note that with some of
  1298. these types of feature flag the bulk of the unreleased functionality itself might
  1299. actually be publicly exposed, but sitting at a url which is not discoverable by
  1300. users.</p>
  1301. </div>
  1302. <div id="TogglesInTheCore">
  1303. <h4>Toggles in the core </h4>
  1304. <p>There are other types of lower-level toggle which must be placed deeper
  1305. within your architecture. These toggles are usually technical in nature, and
  1306. control how some functionality is implemented internally. An example would be a
  1307. Release Toggle which controls whether to use a new piece of caching
  1308. infrastructure in front of a third-party API or just route requests directly to
  1309. that API. Localizing these toggling decisions within the service whose
  1310. functionality is being toggled is the only sensible option in these cases.</p>
  1311. </div>
  1312. </div>
  1313. <div id="ManagingTheCarryingCostOfFeatureToggles">
  1314. <h3>Managing the carrying cost of Feature Toggles</h3>
  1315. <p>Feature Flags have a tendency to multiply rapidly, particularly when first
  1316. introduced. They are useful and cheap to create and so often a lot are created.
  1317. However toggles do come with a carrying cost. They require you to introduce new
  1318. abstractions or conditional logic into your code. They also introduce a
  1319. significant testing burden. Knight Capital Group's <a href="http://dougseven.com/2014/04/17/knightmare-a-devops-cautionary-tale/">$460 million dollar
  1320. mistake</a>
  1321. serves as a cautionary tale on what can go wrong when you don't manage your
  1322. feature flags correctly (amongst other things).</p>
  1323. <div class="soundbite">
  1324. <p>Savvy teams view their Feature Toggles as inventory which comes with a carrying cost, and work to keep that inventory as low as possible.</p>
  1325. </div>
  1326. <p>Savvy teams view the Feature Toggles in their codebase as inventory which comes
  1327. with a carrying cost and seek to keep that inventory as low as possible.
  1328. In order to keep the number of feature flags manageable a team
  1329. must be proactive in removing feature flags that are no longer needed. Some
  1330. teams have a rule of always adding a toggle removal task onto the team's backlog
  1331. whenever a Release Toggle is first introduced. Other teams put "expiration dates"
  1332. on their toggles. Some go as far as creating "time bombs" which will fail a test
  1333. (or even refuse to start an application!) if a feature flag is still around after its
  1334. expiration date. We can also apply a Lean approach to reducing inventory, placing
  1335. a limit on the number of feature flags a system is allowed to have at any one time. Once
  1336. that limit is reached if someone wants to add a new toggle they will first need to
  1337. do the work to remove an existing flag.</p>
  1338. </div>
  1339. </div>
  1340. </article>
  1341. </section>
  1342. <nav id="jumpto">
  1343. <p>
  1344. <a href="/david/blog/">Accueil du blog</a> |
  1345. <a href="https://martinfowler.com/articles/feature-toggles.html">Source originale</a> |
  1346. <a href="/david/stream/2019/">Accueil du flux</a>
  1347. </p>
  1348. </nav>
  1349. <footer>
  1350. <div>
  1351. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  1352. <p>
  1353. Bonjour/Hi!
  1354. Je suis <a href="/david/" title="Profil public">David&nbsp;Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
  1355. Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
  1356. </p>
  1357. <p>
  1358. Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
  1359. </p>
  1360. <p>
  1361. Voici quelques articles choisis :
  1362. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  1363. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  1364. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  1365. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  1366. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  1367. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  1368. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  1369. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  1370. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  1371. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  1372. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  1373. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  1374. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  1375. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  1376. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  1377. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  1378. </p>
  1379. <p>
  1380. On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
  1381. </p>
  1382. <p>
  1383. Je ne traque pas ta navigation mais mon
  1384. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  1385. conserve des logs d’accès.
  1386. </p>
  1387. </div>
  1388. </footer>
  1389. <script type="text/javascript">
  1390. ;(_ => {
  1391. const jumper = document.getElementById('jumper')
  1392. jumper.addEventListener('click', e => {
  1393. e.preventDefault()
  1394. const anchor = e.target.getAttribute('href')
  1395. const targetEl = document.getElementById(anchor.substring(1))
  1396. targetEl.scrollIntoView({behavior: 'smooth'})
  1397. })
  1398. })()
  1399. </script>