[{"data":1,"prerenderedAt":964},["ShallowReactive",2],{"authors":3,"article-2022-02-17-tracking-configuration-updates-over-time":331},[4,23,35,48,61,73,85,98,111,124,136,148,161,173,185,197,209,221,233,245,258,270,282,295,307,319],{"id":5,"title":6,"body":7,"description":11,"extension":14,"meta":15,"name":16,"navigation":17,"path":18,"readingTime":19,"seo":20,"stem":21,"__hash__":22},"authors\u002Fauthors\u002Falexandre-guillon.md","Software Engineer",{"type":8,"value":9,"toc":10},"minimark",[],{"title":11,"searchDepth":12,"depth":12,"links":13},"",2,[],"md",{},"Alexandre Guillon",true,"\u002Fauthors\u002Falexandre-guillon",1,{"title":6,"description":11},"authors\u002Falexandre-guillon","4tf48mjyjFNqItOHaulICbrjeCyMag1o6801uHeTz98",{"id":24,"title":6,"body":25,"description":11,"extension":14,"meta":29,"name":30,"navigation":17,"path":31,"readingTime":19,"seo":32,"stem":33,"__hash__":34},"authors\u002Fauthors\u002Falexis-ablain.md",{"type":8,"value":26,"toc":27},[],{"title":11,"searchDepth":12,"depth":12,"links":28},[],{},"Alexis Ablain","\u002Fauthors\u002Falexis-ablain",{"title":6,"description":11},"authors\u002Falexis-ablain","_SIAtB7f-39e5t3GiJof81NP47s6MGo2n4gaHkTy1uQ",{"id":36,"title":37,"body":38,"description":11,"extension":14,"meta":42,"name":43,"navigation":17,"path":44,"readingTime":19,"seo":45,"stem":46,"__hash__":47},"authors\u002Fauthors\u002Faxel-shaita.md","Engineering Manager",{"type":8,"value":39,"toc":40},[],{"title":11,"searchDepth":12,"depth":12,"links":41},[],{},"Axel Shaïta","\u002Fauthors\u002Faxel-shaita",{"title":37,"description":11},"authors\u002Faxel-shaita","fK0argUhsBkWLjpTAhY13oYLVzQthcEYkCEdtHWmIgE",{"id":49,"title":50,"body":51,"description":11,"extension":14,"meta":55,"name":56,"navigation":17,"path":57,"readingTime":19,"seo":58,"stem":59,"__hash__":60},"authors\u002Fauthors\u002Fbaptiste-faure.md","Head of Talent Acquisition",{"type":8,"value":52,"toc":53},[],{"title":11,"searchDepth":12,"depth":12,"links":54},[],{},"Baptiste Faure","\u002Fauthors\u002Fbaptiste-faure",{"title":50,"description":11},"authors\u002Fbaptiste-faure","ELisToYtcgHmgdVWZkCclTPV6exZtfyXqhpx1jjbJHs",{"id":62,"title":6,"body":63,"description":11,"extension":14,"meta":67,"name":68,"navigation":17,"path":69,"readingTime":19,"seo":70,"stem":71,"__hash__":72},"authors\u002Fauthors\u002Fbenjamin-bouillot.md",{"type":8,"value":64,"toc":65},[],{"title":11,"searchDepth":12,"depth":12,"links":66},[],{},"Benjamin Bouillot","\u002Fauthors\u002Fbenjamin-bouillot",{"title":6,"description":11},"authors\u002Fbenjamin-bouillot","tbhCFZyfTt7ZM5b5YgqQ2nhgnSTl8BweaQQryc87fHo",{"id":74,"title":37,"body":75,"description":11,"extension":14,"meta":79,"name":80,"navigation":17,"path":81,"readingTime":19,"seo":82,"stem":83,"__hash__":84},"authors\u002Fauthors\u002Fcedric-nicoloso.md",{"type":8,"value":76,"toc":77},[],{"title":11,"searchDepth":12,"depth":12,"links":78},[],{},"Cédric Nicoloso","\u002Fauthors\u002Fcedric-nicoloso",{"title":37,"description":11},"authors\u002Fcedric-nicoloso","ibSoh4VZYiWYTuLOnZTedaAfcnvet1Q9H7ogW0LgorY",{"id":86,"title":87,"body":88,"description":11,"extension":14,"meta":92,"name":93,"navigation":17,"path":94,"readingTime":19,"seo":95,"stem":96,"__hash__":97},"authors\u002Fauthors\u002Fdavid-touzet.md","Staff Engineer",{"type":8,"value":89,"toc":90},[],{"title":11,"searchDepth":12,"depth":12,"links":91},[],{},"David Touzet","\u002Fauthors\u002Fdavid-touzet",{"title":87,"description":11},"authors\u002Fdavid-touzet","dHWwnQxb1Ubt-WwXWEODGEo9AFoq1cJUhfg3kdnYSBM",{"id":99,"title":100,"body":101,"description":11,"extension":14,"meta":105,"name":106,"navigation":17,"path":107,"readingTime":19,"seo":108,"stem":109,"__hash__":110},"authors\u002Fauthors\u002Feloise-chizat.md","Data Engineer",{"type":8,"value":102,"toc":103},[],{"title":11,"searchDepth":12,"depth":12,"links":104},[],{},"Eloïse Chizat","\u002Fauthors\u002Feloise-chizat",{"title":100,"description":11},"authors\u002Feloise-chizat","Utd72Vm9qT4hh2ZbFi6a2_nXw5Wb494Ed_HL1ra5yw8",{"id":112,"title":113,"body":114,"description":11,"extension":14,"meta":118,"name":119,"navigation":17,"path":120,"readingTime":19,"seo":121,"stem":122,"__hash__":123},"authors\u002Fauthors\u002Femmanuel-auclair.md","Staff engineer",{"type":8,"value":115,"toc":116},[],{"title":11,"searchDepth":12,"depth":12,"links":117},[],{},"Emmanuel Auclair","\u002Fauthors\u002Femmanuel-auclair",{"title":113,"description":11},"authors\u002Femmanuel-auclair","MtsA8THNLEn0dTtYEIQaGwDuf7MjQL55IOeei5gugEg",{"id":125,"title":6,"body":126,"description":11,"extension":14,"meta":130,"name":131,"navigation":17,"path":132,"readingTime":19,"seo":133,"stem":134,"__hash__":135},"authors\u002Fauthors\u002Fhoreb-parraud.md",{"type":8,"value":127,"toc":128},[],{"title":11,"searchDepth":12,"depth":12,"links":129},[],{},"Horeb Parraud","\u002Fauthors\u002Fhoreb-parraud",{"title":6,"description":11},"authors\u002Fhoreb-parraud","ajjsnUX4ohZI-ghMdbb92q_taWDkKXVZSLZXoAeLQtg",{"id":137,"title":37,"body":138,"description":11,"extension":14,"meta":142,"name":143,"navigation":17,"path":144,"readingTime":19,"seo":145,"stem":146,"__hash__":147},"authors\u002Fauthors\u002Fhugo-contreras.md",{"type":8,"value":139,"toc":140},[],{"title":11,"searchDepth":12,"depth":12,"links":141},[],{},"Hugo Contreras","\u002Fauthors\u002Fhugo-contreras",{"title":37,"description":11},"authors\u002Fhugo-contreras","2nc3VMu9ASq9Z6Pwx2-7-Ye991Pww4p-UEDBQFfjF-Q",{"id":149,"title":150,"body":151,"description":11,"extension":14,"meta":155,"name":156,"navigation":17,"path":157,"readingTime":19,"seo":158,"stem":159,"__hash__":160},"authors\u002Fauthors\u002Fjulien-tassin.md","Head of Engineering",{"type":8,"value":152,"toc":153},[],{"title":11,"searchDepth":12,"depth":12,"links":154},[],{},"Julien Tassin","\u002Fauthors\u002Fjulien-tassin",{"title":150,"description":11},"authors\u002Fjulien-tassin","iUIHI7SITje38Jh9X9uvYs4-VsHx4eCdt6hAlyLFG_o",{"id":162,"title":6,"body":163,"description":11,"extension":14,"meta":167,"name":168,"navigation":17,"path":169,"readingTime":19,"seo":170,"stem":171,"__hash__":172},"authors\u002Fauthors\u002Flaurent-renard.md",{"type":8,"value":164,"toc":165},[],{"title":11,"searchDepth":12,"depth":12,"links":166},[],{},"Laurent Renard","\u002Fauthors\u002Flaurent-renard",{"title":6,"description":11},"authors\u002Flaurent-renard","5BP7Ed-pt1SQHjh0UJ1XUrlLTcdlFaDoKBCP4deHq8A",{"id":174,"title":6,"body":175,"description":11,"extension":14,"meta":179,"name":180,"navigation":17,"path":181,"readingTime":19,"seo":182,"stem":183,"__hash__":184},"authors\u002Fauthors\u002Fleo-martin.md",{"type":8,"value":176,"toc":177},[],{"title":11,"searchDepth":12,"depth":12,"links":178},[],{},"Léo Martin","\u002Fauthors\u002Fleo-martin",{"title":6,"description":11},"authors\u002Fleo-martin","eYxCHkRgbGDV7shKdTA9s7Tu0zGV4yDGFoKR5MHQntY",{"id":186,"title":6,"body":187,"description":11,"extension":14,"meta":191,"name":192,"navigation":17,"path":193,"readingTime":19,"seo":194,"stem":195,"__hash__":196},"authors\u002Fauthors\u002Floic-bousquet.md",{"type":8,"value":188,"toc":189},[],{"title":11,"searchDepth":12,"depth":12,"links":190},[],{},"Loïc Bousquet","\u002Fauthors\u002Floic-bousquet",{"title":6,"description":11},"authors\u002Floic-bousquet","ko12qZwiGL8XNjAoy9oWypPkIjr29Pbq7vhdtgldqeQ",{"id":198,"title":6,"body":199,"description":11,"extension":14,"meta":203,"name":204,"navigation":17,"path":205,"readingTime":19,"seo":206,"stem":207,"__hash__":208},"authors\u002Fauthors\u002Floic-poullain.md",{"type":8,"value":200,"toc":201},[],{"title":11,"searchDepth":12,"depth":12,"links":202},[],{},"Loïc Poullain","\u002Fauthors\u002Floic-poullain",{"title":6,"description":11},"authors\u002Floic-poullain","oRIyJhFRTqxy5dLCYQ2OnYZ1DB-gLDUM-85vTSYuTF0",{"id":210,"title":100,"body":211,"description":11,"extension":14,"meta":215,"name":216,"navigation":17,"path":217,"readingTime":19,"seo":218,"stem":219,"__hash__":220},"authors\u002Fauthors\u002Fmaud-lelu.md",{"type":8,"value":212,"toc":213},[],{"title":11,"searchDepth":12,"depth":12,"links":214},[],{},"Maud Lélu","\u002Fauthors\u002Fmaud-lelu",{"title":100,"description":11},"authors\u002Fmaud-lelu","MMbsCKuE41OMHusrl12FIEsI-Trx7l8Nn_ANhvj2_y4",{"id":222,"title":37,"body":223,"description":11,"extension":14,"meta":227,"name":228,"navigation":17,"path":229,"readingTime":19,"seo":230,"stem":231,"__hash__":232},"authors\u002Fauthors\u002Fnicolas-poirier.md",{"type":8,"value":224,"toc":225},[],{"title":11,"searchDepth":12,"depth":12,"links":226},[],{},"Nicolas Poirier","\u002Fauthors\u002Fnicolas-poirier",{"title":37,"description":11},"authors\u002Fnicolas-poirier","dXrJkYo8az4SN_D23aYc3fQ7z8s1dR2a0lt1ogjAjJs",{"id":234,"title":37,"body":235,"description":11,"extension":14,"meta":239,"name":240,"navigation":17,"path":241,"readingTime":19,"seo":242,"stem":243,"__hash__":244},"authors\u002Fauthors\u002Fraphael-sauget.md",{"type":8,"value":236,"toc":237},[],{"title":11,"searchDepth":12,"depth":12,"links":238},[],{},"Raphaël Sauget","\u002Fauthors\u002Fraphael-sauget",{"title":37,"description":11},"authors\u002Fraphael-sauget","Uri9bcq0QDuxRA0PbBoNtu7p_5L3dALu4kzcXVW0xyM",{"id":246,"title":247,"body":248,"description":11,"extension":14,"meta":252,"name":253,"navigation":17,"path":254,"readingTime":19,"seo":255,"stem":256,"__hash__":257},"authors\u002Fauthors\u002Fromain-koenig.md","Co-funder & Head of innovation",{"type":8,"value":249,"toc":250},[],{"title":11,"searchDepth":12,"depth":12,"links":251},[],{},"Romain Koenig","\u002Fauthors\u002Fromain-koenig",{"title":247,"description":11},"authors\u002Fromain-koenig","uyS8--eG2_ezyqRABcJnMJmQKKuSArhPWd14aUvFeEw",{"id":259,"title":37,"body":260,"description":11,"extension":14,"meta":264,"name":265,"navigation":17,"path":266,"readingTime":19,"seo":267,"stem":268,"__hash__":269},"authors\u002Fauthors\u002Fromaric-juniet.md",{"type":8,"value":261,"toc":262},[],{"title":11,"searchDepth":12,"depth":12,"links":263},[],{},"Romaric Juniet","\u002Fauthors\u002Fromaric-juniet",{"title":37,"description":11},"authors\u002Fromaric-juniet","4Zb2artgT-eo-PHLXi3xi4d5t7s6PfhUxeSfXIikSUY",{"id":271,"title":6,"body":272,"description":11,"extension":14,"meta":276,"name":277,"navigation":17,"path":278,"readingTime":19,"seo":279,"stem":280,"__hash__":281},"authors\u002Fauthors\u002Fstanyslas-bres.md",{"type":8,"value":273,"toc":274},[],{"title":11,"searchDepth":12,"depth":12,"links":275},[],{},"Stanyslas Bres","\u002Fauthors\u002Fstanyslas-bres",{"title":6,"description":11},"authors\u002Fstanyslas-bres","Xa0SahETuiN4q1jrmR2ych3moAqcZ2LbU7vSfEt2RuU",{"id":283,"title":284,"body":285,"description":11,"extension":14,"meta":289,"name":290,"navigation":17,"path":291,"readingTime":19,"seo":292,"stem":293,"__hash__":294},"authors\u002Fauthors\u002Ftalent-acquisition.md","Talent Acquisition",{"type":8,"value":286,"toc":287},[],{"title":11,"searchDepth":12,"depth":12,"links":288},[],{},"Équipe Talent Acquisition","\u002Fauthors\u002Ftalent-acquisition",{"description":11},"authors\u002Ftalent-acquisition","doDfE76txftQ4wIiKjJoDmSpyzSKk0tzlgVAp6-opAY",{"id":296,"title":6,"body":297,"description":11,"extension":14,"meta":301,"name":302,"navigation":17,"path":303,"readingTime":19,"seo":304,"stem":305,"__hash__":306},"authors\u002Fauthors\u002Fvictor-borg.md",{"type":8,"value":298,"toc":299},[],{"title":11,"searchDepth":12,"depth":12,"links":300},[],{},"Victor Borg","\u002Fauthors\u002Fvictor-borg",{"title":6,"description":11},"authors\u002Fvictor-borg","-Za-JweoiP6hyclue_WkxMXdRUDTczPGlJf6AZckjUc",{"id":308,"title":6,"body":309,"description":11,"extension":14,"meta":313,"name":314,"navigation":17,"path":315,"readingTime":19,"seo":316,"stem":317,"__hash__":318},"authors\u002Fauthors\u002Fvirgil-roger.md",{"type":8,"value":310,"toc":311},[],{"title":11,"searchDepth":12,"depth":12,"links":312},[],{},"Virgil Roger","\u002Fauthors\u002Fvirgil-roger",{"title":6,"description":11},"authors\u002Fvirgil-roger","DfVFe5j0bCgXeEr381ZYOM5DP4m-pWb93J9-m_muKJ0",{"id":320,"title":6,"body":321,"description":11,"extension":14,"meta":325,"name":326,"navigation":17,"path":327,"readingTime":19,"seo":328,"stem":329,"__hash__":330},"authors\u002Fauthors\u002Fyukan-zhao.md",{"type":8,"value":322,"toc":323},[],{"title":11,"searchDepth":12,"depth":12,"links":324},[],{},"Yukan Zhao","\u002Fauthors\u002Fyukan-zhao",{"title":6,"description":11},"authors\u002Fyukan-zhao","LRPHugtAJnWHsmHxy9_SR5Zas_C5p-GR_uHEs1Fhk_E",{"id":332,"title":333,"author":334,"body":335,"date":954,"description":955,"extension":14,"lang":956,"meta":957,"navigation":17,"path":958,"published":17,"readingTime":533,"seo":959,"stem":960,"tags":961,"__hash__":963},"articles\u002Farticles\u002F2022-02-17-tracking-configuration-updates-over-time.md","Tracking configuration updates over time","victor-borg",{"type":8,"value":336,"toc":948},[337,341,344,347,350,366,378,383,386,389,410,413,419,422,426,437,440,647,650,653,658,661,664,668,671,678,924,927,931,935,938,941,944],[338,339,340],"p",{},"When we think of user configuration, we usually represent it as a fixed and global value. For\nexample, a user only has a single email address and has a preferred display language. It can be\nupdated at any time, replacing the old value. When we need to send an email, we just have to look\nfor these values and we don’t need to know what the previous values were, we only need the latest\nconfiguration.",[338,342,343],{},"However, we may need to know what these configurations were at any given time. At Indy, our users\nmay change their fiscal preferences depending on their obligations across the years. Current user\nsettings may not be the same as they were 6 months ago, but we still have to know and support\nprevious configurations in case the user needs to amend their previous declarations.",[338,345,346],{},"This requirement first arose for VAT (value added tax) declarations: given your profits and your\npersonal choices, you may or may not be required to declare your VAT. And if you are, you can either\ndeclare them monthly, quarterly or yearly. This has various implications across the application:\nsome tax calculations and forms need to know if the user is liable to VAT. At first, we naively\nstored this information as any other basic configuration value, overwriting previous values at each\nupdate. As a result, when a user needed to make adjustments to a previous tax declaration, let’s say\nfor fiscal year 2019, and VAT configuration was updated in the meantime for fiscal year 2020, we\nmistakenly took this new value into account for 2019.",[338,348,349],{},"We needed to find a way to store what we call “historized configuration values”: we must be able to\nanswer this kind of questions:",[351,352,353,357,360,363],"ul",{},[354,355,356],"li",{},"“Was the user liable to VAT on September 17th, 2020?”",[354,358,359],{},"“Was the user liable to VAT at least once in 2021?”",[354,361,362],{},"“Since when has the user been liable to VAT?”",[354,364,365],{},"“On what period does the user have to make monthly VAT declarations?”",[367,368,370,371,377],"div",{"style":369},"padding: 0.75rem; border-radius: 0.375rem; background-color: #D6F3F7;","\n  ",[372,373,376],"span",{"className":374},[375],"font-semibold","ℹ️","\n  As we use MongoDB for our app database, we use the term « document ». It can be read as « row » in a SQL database context.\n",[379,380,382],"h2",{"id":381},"the-naive-solution","The naive solution",[338,384,385],{},"The first “obvious” solution that comes to mind is to store a configuration timeline for each user,\nan array of objects containing configuration values and their validity timeframes (start and end\ndates). At signup, we create for the user a single document containing an initial start date, for\nexample in 1970, and no end date, as this is the user current configuration. When the user makes an\nupdate, they now have to provide the date from which the change is effective. We create a new\ndocument with the start date being the provided effective date, and update the previous document by\nsetting the end date to the new configuration start date. Later on, when we need to get\nconfiguration at a given date, we just have to look for the configuration document that contains it\nwithin its time range.",[338,387,388],{},"But this solution has a major flaw: we need to take great care in updating our configuration\ndocuments, ensuring that the timeline is complete, each configuration end date being the start date\nof the following configuration. As configuration updates can be retroactive, we may even have to\ndelete whole documents if the new one completely overlaps their time range. So at each configuration\nupdate, we have to make up to three different operation types, that can damage timeline integrity if\ndone incorrectly :",[351,390,391,398,404],{},[354,392,393,397],{},[394,395,396],"strong",{},"Insertion"," for the new configuration document",[354,399,400,403],{},[394,401,402],{},"Updates"," for previous documents, updating their timeframe to ensure a gapless timeline",[354,405,406,409],{},[394,407,408],{},"Deletion"," of “shadowed” configuration documents when their period is overridden by the new one",[338,411,412],{},"Here is a little diagram illustrating an update scenario: if we create the new orange configuration\nstarting before the blue end date, we have to update its end date and delete the green and red\ndocuments as they are completely overwritten by orange.",[338,414,415],{},[416,417],"img",{"alt":11,"src":418},"\u002Fimages\u002Funtitled-3.png",[338,420,421],{},"On the other hand, accessing configuration values is trivial, as simple as a database request with\ndate filters. But we preferred to go on with another solution that makes configuration access\nslightly more complex, in exchange for data integrity and consistency guarantees.",[379,423,425],{"id":424},"the-immutable-solution","The immutable solution",[338,427,428,429,436],{},"Our final solution was heavily inspired by\n",[430,431,435],"a",{"href":432,"rel":433},"https:\u002F\u002Fmartinfowler.com\u002FeaaDev\u002FTemporalProperty.html",[434],"nofollow","Martin Fowler’s Temporal Property article",".\nHe describes how we can hold configuration values on what he calls Value objects, with an\nEffectivity property, describing when this value is effective. We took his approach and terminology\nwith an event sourcing pattern. We wanted each configuration update to imply a single document\ninsertion in our database, without needing any form of updates on previous ones. This way, we ensure\ndata consistency and minimise the risk of errors messing up with user configurations.",[338,438,439],{},"We ended up with this kind of configuration document:",[441,442,446],"pre",{"className":443,"code":444,"language":445,"meta":11,"style":11},"language-json shiki shiki-themes github-light github-dark","{\n  \"_id\": \"random_id\",\n  \"user_id\": \"user_id\",\n\n  \u002F\u002F The actual configuration payload\n  \"configuration\": {\n    \"vat_selected\": \"vat_ht\",\n    \"vat_frequency\": \"monthly\"\n  },\n\n  \u002F\u002F Auto generated value, being this document creation date\n  \"known_at\": {\n    \"$date\": \"2020-01-20T15:37:14.547Z\"\n  },\n\n  \u002F\u002F User controlled value, telling when this configuration starts being active\n  \"effective_date\": {\n    \"$date\": \"2000-01-01T15:37:14.547Z\"\n  },\n\n  \u002F\u002F *Optional* user controlled value, telling when this configuration will expire and not be active anymore\n  \"end_date\": {\n    \"$date\": \"2021-01-01T15:37:14.547Z\"\n  }\n}\n","json",[447,448,449,456,472,485,491,498,507,520,531,537,542,548,556,567,572,577,583,591,601,606,611,617,625,635,641],"code",{"__ignoreMap":11},[372,450,452],{"class":451,"line":19},"line",[372,453,455],{"class":454},"sVt8B","{\n",[372,457,458,462,465,469],{"class":451,"line":12},[372,459,461],{"class":460},"sj4cs","  \"_id\"",[372,463,464],{"class":454},": ",[372,466,468],{"class":467},"sZZnC","\"random_id\"",[372,470,471],{"class":454},",\n",[372,473,475,478,480,483],{"class":451,"line":474},3,[372,476,477],{"class":460},"  \"user_id\"",[372,479,464],{"class":454},[372,481,482],{"class":467},"\"user_id\"",[372,484,471],{"class":454},[372,486,488],{"class":451,"line":487},4,[372,489,490],{"emptyLinePlaceholder":17},"\n",[372,492,494],{"class":451,"line":493},5,[372,495,497],{"class":496},"sJ8bj","  \u002F\u002F The actual configuration payload\n",[372,499,501,504],{"class":451,"line":500},6,[372,502,503],{"class":460},"  \"configuration\"",[372,505,506],{"class":454},": {\n",[372,508,510,513,515,518],{"class":451,"line":509},7,[372,511,512],{"class":460},"    \"vat_selected\"",[372,514,464],{"class":454},[372,516,517],{"class":467},"\"vat_ht\"",[372,519,471],{"class":454},[372,521,523,526,528],{"class":451,"line":522},8,[372,524,525],{"class":460},"    \"vat_frequency\"",[372,527,464],{"class":454},[372,529,530],{"class":467},"\"monthly\"\n",[372,532,534],{"class":451,"line":533},9,[372,535,536],{"class":454},"  },\n",[372,538,540],{"class":451,"line":539},10,[372,541,490],{"emptyLinePlaceholder":17},[372,543,545],{"class":451,"line":544},11,[372,546,547],{"class":496},"  \u002F\u002F Auto generated value, being this document creation date\n",[372,549,551,554],{"class":451,"line":550},12,[372,552,553],{"class":460},"  \"known_at\"",[372,555,506],{"class":454},[372,557,559,562,564],{"class":451,"line":558},13,[372,560,561],{"class":460},"    \"$date\"",[372,563,464],{"class":454},[372,565,566],{"class":467},"\"2020-01-20T15:37:14.547Z\"\n",[372,568,570],{"class":451,"line":569},14,[372,571,536],{"class":454},[372,573,575],{"class":451,"line":574},15,[372,576,490],{"emptyLinePlaceholder":17},[372,578,580],{"class":451,"line":579},16,[372,581,582],{"class":496},"  \u002F\u002F User controlled value, telling when this configuration starts being active\n",[372,584,586,589],{"class":451,"line":585},17,[372,587,588],{"class":460},"  \"effective_date\"",[372,590,506],{"class":454},[372,592,594,596,598],{"class":451,"line":593},18,[372,595,561],{"class":460},[372,597,464],{"class":454},[372,599,600],{"class":467},"\"2000-01-01T15:37:14.547Z\"\n",[372,602,604],{"class":451,"line":603},19,[372,605,536],{"class":454},[372,607,609],{"class":451,"line":608},20,[372,610,490],{"emptyLinePlaceholder":17},[372,612,614],{"class":451,"line":613},21,[372,615,616],{"class":496},"  \u002F\u002F *Optional* user controlled value, telling when this configuration will expire and not be active anymore\n",[372,618,620,623],{"class":451,"line":619},22,[372,621,622],{"class":460},"  \"end_date\"",[372,624,506],{"class":454},[372,626,628,630,632],{"class":451,"line":627},23,[372,629,561],{"class":460},[372,631,464],{"class":454},[372,633,634],{"class":467},"\"2021-01-01T15:37:14.547Z\"\n",[372,636,638],{"class":451,"line":637},24,[372,639,640],{"class":454},"  }\n",[372,642,644],{"class":451,"line":643},25,[372,645,646],{"class":454},"}\n",[338,648,649],{},"Reading a configuration value at a given date consists of finding the most recent document (greatest\nknown_at date) with an effective date before the given date, and an end date after the given date\n(if it exists). This does the job for our simplest requirement “I want to know what is the\nconfiguration at this specific point in time”, but the other requirements are a bit tougher to meet\nif we directly work with raw documents.",[338,651,652],{},"Instead, we create an intermediate representation that looks like the “naive timeline” we talked\nabout earlier, which is way easier to work with.",[338,654,655],{},[416,656],{"alt":11,"src":657},"\u002Fimages\u002Fksmavzydjunodd8jqjufyn6nabo6oqzf7v37rkge4swf6ub17k9oqjkprujgezuf7zcnz-dyv9mb6tvlbuoylkz6rzpfla5mtgynpuz5umxqpmluzpvafdrlyodbhid2k6equfars0.png",[338,659,660],{},"Temporal documents are represented here as colored lines (blue, green and red), each color being a\nsingle value, valid for the given time range. All of them have an effective date, being the dot at\nthe beginning, and an optional end date, being the dot at the end.",[338,662,663],{},"From this raw representation of our configuration documents, we can construct a simpler, cleaner\ntimeline (the colored rectangles at the bottom). From here, we can meet all of our requirements, and\neasily work through time and configurations! And icing on the cake, we can even reconstruct this\nfinal timeline as it was in the past; we only need to filter out documents that were created after a\ngiven date.",[379,665,667],{"id":666},"the-implementation","The implementation",[338,669,670],{},"We have multiple micro-services that need to access configuration histories, so we made a standalone\ninternal npm package.",[338,672,673,674,677],{},"It is a collection of pure functions that can be instantiated by providing an array of raw\nconfiguration documents. In order to be interoperable with any type of configuration, it only relies\non the presence of an effective date, end date and first known date on each document (the\n",[447,675,676],{},"TemporalDocument"," type). So it can work with VAT configurations or anything else. Once\ninstantiated, you get an HistoryService having the final timeline internal representation, and only\nexposes a set of generic functions that operate on it:",[441,679,683],{"className":680,"code":681,"language":682,"meta":11,"style":11},"language-typescript shiki shiki-themes github-light github-dark","export interface HistoryService\u003CT extends TemporalDocument> {\n  \u002F**\n   * Effective configuration at current date.\n   * Returns `undefined` if no document is effective at that date.\n   *\u002F\n  getCurrentConfiguration(): TimelineItem\u003CT> | undefined;\n\n  \u002F**\n   * Effective configuration at given date.\n   * Returns `undefined` if no document is effective at that date.\n   *\u002F\n  getConfigurationAtDate({ date }: { date: Date }): TimelineItem\u003CT> | undefined;\n\n  \u002F**\n   * An array of effective documents on given date range.\n   * Returns an empty array if there is no effective documents on provided date range.\n   *\u002F\n  getConfigurationsOnDateRange({\n    startDate,\n    endDate,\n  }: {\n    startDate: Date;\n    endDate: Date;\n  }): TimelineItem\u003CT>[];\n}\n","typescript",[447,684,685,713,718,723,728,733,763,767,771,776,780,784,830,834,838,843,848,852,860,867,874,884,894,904,920],{"__ignoreMap":11},[372,686,687,691,694,698,701,704,707,710],{"class":451,"line":19},[372,688,690],{"class":689},"szBVR","export",[372,692,693],{"class":689}," interface",[372,695,697],{"class":696},"sScJk"," HistoryService",[372,699,700],{"class":454},"\u003C",[372,702,703],{"class":696},"T",[372,705,706],{"class":689}," extends",[372,708,709],{"class":696}," TemporalDocument",[372,711,712],{"class":454},"> {\n",[372,714,715],{"class":451,"line":12},[372,716,717],{"class":496},"  \u002F**\n",[372,719,720],{"class":451,"line":474},[372,721,722],{"class":496},"   * Effective configuration at current date.\n",[372,724,725],{"class":451,"line":487},[372,726,727],{"class":496},"   * Returns `undefined` if no document is effective at that date.\n",[372,729,730],{"class":451,"line":493},[372,731,732],{"class":496},"   *\u002F\n",[372,734,735,738,741,744,747,749,751,754,757,760],{"class":451,"line":500},[372,736,737],{"class":696},"  getCurrentConfiguration",[372,739,740],{"class":454},"()",[372,742,743],{"class":689},":",[372,745,746],{"class":696}," TimelineItem",[372,748,700],{"class":454},[372,750,703],{"class":696},[372,752,753],{"class":454},"> ",[372,755,756],{"class":689},"|",[372,758,759],{"class":460}," undefined",[372,761,762],{"class":454},";\n",[372,764,765],{"class":451,"line":509},[372,766,490],{"emptyLinePlaceholder":17},[372,768,769],{"class":451,"line":522},[372,770,717],{"class":496},[372,772,773],{"class":451,"line":533},[372,774,775],{"class":496},"   * Effective configuration at given date.\n",[372,777,778],{"class":451,"line":539},[372,779,727],{"class":496},[372,781,782],{"class":451,"line":544},[372,783,732],{"class":496},[372,785,786,789,792,796,799,801,804,806,808,811,814,816,818,820,822,824,826,828],{"class":451,"line":550},[372,787,788],{"class":696},"  getConfigurationAtDate",[372,790,791],{"class":454},"({ ",[372,793,795],{"class":794},"s4XuR","date",[372,797,798],{"class":454}," }",[372,800,743],{"class":689},[372,802,803],{"class":454}," { ",[372,805,795],{"class":794},[372,807,743],{"class":689},[372,809,810],{"class":696}," Date",[372,812,813],{"class":454}," })",[372,815,743],{"class":689},[372,817,746],{"class":696},[372,819,700],{"class":454},[372,821,703],{"class":696},[372,823,753],{"class":454},[372,825,756],{"class":689},[372,827,759],{"class":460},[372,829,762],{"class":454},[372,831,832],{"class":451,"line":558},[372,833,490],{"emptyLinePlaceholder":17},[372,835,836],{"class":451,"line":569},[372,837,717],{"class":496},[372,839,840],{"class":451,"line":574},[372,841,842],{"class":496},"   * An array of effective documents on given date range.\n",[372,844,845],{"class":451,"line":579},[372,846,847],{"class":496},"   * Returns an empty array if there is no effective documents on provided date range.\n",[372,849,850],{"class":451,"line":585},[372,851,732],{"class":496},[372,853,854,857],{"class":451,"line":593},[372,855,856],{"class":696},"  getConfigurationsOnDateRange",[372,858,859],{"class":454},"({\n",[372,861,862,865],{"class":451,"line":603},[372,863,864],{"class":794},"    startDate",[372,866,471],{"class":454},[372,868,869,872],{"class":451,"line":608},[372,870,871],{"class":794},"    endDate",[372,873,471],{"class":454},[372,875,876,879,881],{"class":451,"line":613},[372,877,878],{"class":454},"  }",[372,880,743],{"class":689},[372,882,883],{"class":454}," {\n",[372,885,886,888,890,892],{"class":451,"line":619},[372,887,864],{"class":794},[372,889,743],{"class":689},[372,891,810],{"class":696},[372,893,762],{"class":454},[372,895,896,898,900,902],{"class":451,"line":627},[372,897,871],{"class":794},[372,899,743],{"class":689},[372,901,810],{"class":696},[372,903,762],{"class":454},[372,905,906,909,911,913,915,917],{"class":451,"line":637},[372,907,908],{"class":454},"  })",[372,910,743],{"class":689},[372,912,746],{"class":696},[372,914,700],{"class":454},[372,916,703],{"class":696},[372,918,919],{"class":454},">[];\n",[372,921,922],{"class":451,"line":643},[372,923,646],{"class":454},[338,925,926],{},"For our specific use case of VAT configuration, we created a VatHistoryService that wraps our low\nlevel HistoryService, exposing only VAT-oriented functionality, ready to be used where needed in the\napp:",[416,928],{"src":929,"alt":11,"style":930},"\u002Fimages\u002Fhx05e6ckrwycujn85u0sao6tpctgcsbvr-zxxpay8m8umubugdlooblxm-1uoiscbpvf6chlfnr7dymrir7_im_qozejk3bzewydmvc3saixjsmgueev9jx_mqydl3roa6-8p1ijs0.png","height:280px; object-fit:contain; width:100%;",[379,932,934],{"id":933},"wrapping-up","Wrapping up",[338,936,937],{},"It’s been almost a year since we deployed this solution for our users, and we are satisfied with the\nresults. We no longer have issues with stale configurations nor data integrity problems.",[338,939,940],{},"However, this posed some challenges. For instance, at release time, we had to initialize user\nconfiguration for all users, based only on what we knew about them then. There was no way to know\nwhat their previous configurations were, and some users did not even complete their account setup by\nthe time of initialization. Keep this in mind if you adopt this kind of solution on an existing\nsystem: there is no guarantee that stored past configurations are correct for users that signed up\nbefore historization setup.",[338,942,943],{},"Also, it is worth noting that this design makes configuration updates trivial, at the expense of\ndata readability and accessibility. Specific developments have to be made to visualize configuration\nhistory (for debugging and customer support purposes, for example). But we believe that as long as\nour data is safely stored and updated, this is a tradeoff we are willing to make. In case of a bug,\nit is way easier to fix pure read-only functions than having to fix database data.",[945,946,947],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":11,"searchDepth":12,"depth":12,"links":949},[950,951,952,953],{"id":381,"depth":12,"text":382},{"id":424,"depth":12,"text":425},{"id":666,"depth":12,"text":667},{"id":933,"depth":12,"text":934},"2022-02-17","When we think of user configuration, we usually represent it as a fixed and global value.","en",{},"\u002Farticles\u002F2022-02-17-tracking-configuration-updates-over-time",{"title":333,"description":955},"articles\u002F2022-02-17-tracking-configuration-updates-over-time",[962],"Tech","3VCW47aOwUM9hmY0MOpdBgZPuOD1shgiS9zkQv1CTr4",1778159244141]