{"id":1786,"date":"2024-03-05T16:51:04","date_gmt":"2024-03-05T15:51:04","guid":{"rendered":"https:\/\/www.loicmathieu.fr\/wordpress\/?p=1786"},"modified":"2024-03-05T17:01:02","modified_gmt":"2024-03-05T16:01:02","slug":"concevoir-un-saas-multitenant","status":"publish","type":"post","link":"https:\/\/www.loicmathieu.fr\/wordpress\/informatique\/concevoir-un-saas-multitenant\/","title":{"rendered":"Designing a multi-tenant SaaS"},"content":{"rendered":"<p>This article is based on my talk <a href=\"https:\/\/www.youtube.com\/watch?v=JkBT4kVa9D8\" title=\"Designing a multi-tenant SaaS on Youtube\" rel=\"noopener\" target=\"_blank\">Designing a multi-tenant SaaS<\/a> given at Cloud Nord on October 12, 2023.<\/p>\n<p><a href=\"https:\/\/www.kestra.io\" rel=\"noopener\" target=\"_blank\">Kestra<\/a> is a highly scalable data scheduling and orchestration platform that creates, executes, schedules and monitors millions of complex pipelines. For an introduction to Kestra, you can read <a href=\"https:\/\/www.loicmathieu.fr\/wordpress\/informatique\/introduction-a-kestra\/\" title=\"Introduction to Kestra\">my article on the subject<\/a>.<\/p>\n<p>One of the recent Kestra evolutions I was responsible for was multitenancy support, this article will tell you about the design that went into adding this functionality.<\/p>\n<h2>Multitenancy and its different models<\/h2>\n<p>Multitenancy can be defined as a software architecture principle that enables software to serve multiple client organizations (tenants) from a single installation.<\/p>\n<p>Multitenancy simulates several logical instances within a single physical instance. The aim is to control hosting and operating costs.<\/p>\n<p>There are several multi-tenant models.<\/p>\n<h3>One instance per tenant<\/h3>\n<ul><li>One application instance is started for each tenant, multitenancy is managed outside the application.<\/li> \n<li>Pros: simplicity.<\/li> \n<li>Cons: operating cost, you have to start one application per tenant.<\/li>\n<\/ul>\n<h3>One database per tenant<\/h3>\n<ul><li>A database is started for each tenant, the only logic to implement is database selection.<\/li> \n<li>Pros: simplicity, implementation cost.<\/li> \n<li>Cons: operation cost, you have to start one database per tenant.<\/li> \n<\/ul>\n<h3>One schema per tenant<\/h3>\n<ul><li>A database schema is created for each tenant, the only logic to implement is schema selection.<\/li> \n<li>Pros: simplicity, implementation cost, operation cost.<\/li> \n<li>Cons: requires a base offering schemas, the single base is the SPOF.<\/li> \n<\/ul>\n<h3>Tenant within tables\/messages<\/h3>\n<ul><li>A <code>tenantId<\/code> field is added to each row of each table.\/li&gt;\n<\/li>\n<li>Pros: flexibility, cost of operation.<\/li> \n<li>Cons: complexity, cost of implementation.<\/li> \n<\/ul>\n<h2>Kestra&#8217;s architecture<\/h2>\n<p>First of all, let&#8217;s explain Kestra&#8217;s architecture.<\/p>\n<img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/kestra-software-architecture.png?resize=640%2C360&#038;ssl=1\" alt=\"\" width=\"640\" height=\"360\" class=\"alignnone size-large wp-image-1788\" srcset=\"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/kestra-software-architecture.png?resize=1024%2C576&amp;ssl=1 1024w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/kestra-software-architecture.png?resize=300%2C169&amp;ssl=1 300w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/kestra-software-architecture.png?resize=768%2C432&amp;ssl=1 768w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/kestra-software-architecture.png?resize=480%2C270&amp;ssl=1 480w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/kestra-software-architecture.png?w=1200&amp;ssl=1 1200w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/>\n<p>Kestra is separated into several components that communicate with each other by asynchronous messages via a queue. Flow metadatas are stored in a repository.<\/p>\n<p>The various components are:<\/p>\n<ul><li>The <strong>Executor<\/strong>: contains the orchestration logic.<\/li> \n<li>The <strong>Scheduler<\/strong>: handles the various trigger events of a flow.<\/li> \n<li>The <strong>Worker<\/strong>: executes the tasks of a flow.<\/li> \n<li>The <strong>Indexer<\/strong>: optional component that indexes the queue into the database.<\/li> \n<li>The <strong>Webserver<\/strong>: serves Kestra&#8217;s GUI and API.<\/li> \n<\/ul>\n<p>The <strong>Worker<\/strong> is the only component that access to external systems needed to execute a flow (remote database, web service, cloud service, &#8230;) as well as Kestra&#8217;s internal data storage.<\/p>\n<p>Kestra offers two deployment modes: standalone with all components in a single process, or microservice with one component per process.<\/p>\n<p>Kestra offers two runners:<\/p>\n<ul><li>The <strong>JDBC<\/strong> runner: Queue and Repository are implemented via a database (H2, PostgreSQL, MySQL).<\/li> \n<li>The <strong>Kafka<\/strong>runner: Kafka is used as Queue and Elasticsearch as Repository. This runner is only available in the enterprise edition.<\/li> <\/ul>\n<h2>Multitenancy at Kestra<\/h2>\n<h3>The SaaS project<\/h3>\n<p>Kestra is working on a version available in a SaaS, Kestra Cloud.<\/p>\n<p>To date, the constraints of the SaaS are as follows:<\/p>\n<ul><li>A &#8220;big&#8221; high dispo cluster with one Kafka runner per cloud provider \/ region.<\/li> \n<li>Each tenant has its own resources (namespace, flow, execution).<\/li> \n<li>Data isolation.<\/li> \n<li>A user is global to all tenants.<\/li> \n<\/ul>\n<h3>The notion of namespace<\/h3>\n<p>A flow is in a namespace; namespaces are hierarchical, like a filesystem directory.<\/p>\n<p>Namespaces enable specific configuration (task, secret, &#8230;) as well as the definition of role-based access in the enterprise version.<\/p>\n<p>One of the questions we asked ourselves was whether the notion of namespace could be of use to us in implementing Kestra multitenancy.<\/p>\n<h3>The evaluated models<\/h3>\n<p>Three different multitenancy models were evaluated.<\/p>\n<ol><li><strong>Tenant per namespace<\/strong>: each namespace is a tenant. This solution is close to the <em>One schema per tenant<\/em> solution, but using namespace, which is a property specific to Kestra.<\/li> \n<li><strong>Tenant per base namespace<\/strong>: each base namespace is a tenant. A tenant can have several namespaces, the children of the base namespace. This is a variation on the previous model.<\/li> \n<li>Tenant via a <code>tenantId<\/code> property.<\/li> \n<\/ol>\n<p>As one of Kestra&#8217;s runners uses Kafka and Elasticsearch, which do not support the notion of schema, only a declination of the <em>Tenant model within tables\/messages<\/em> was possible. The three solutions therefore propose adding the tenant to a new property or using an existing property (namespace) to limit the changes required.<\/p>\n<h3>The choice<\/h3>\n<p><strong>Tenant via a new <code>tenantId<\/code> property.<\/strong><\/p>\n<p>Using namespace would have been convenient, as we already had namespace-based flow isolation as well as role-based access management. But it would have greatly reduced the functionality of a user of our Cloud, as a namespace or basic namespace could not have been used by different users. So the only model that met all our needs without limiting future functionalities was the addition of a new <code>tenantId<\/code> property.<\/p>\n<h2>Implementation<\/h2>\n<ol><li>We add a property <code>tenantId<\/code> to all model objects.<\/li> \n<li>We filter on the <code>tenantId<\/code> column in every BDD request.<\/li> \n<li>We resolve the <code>tenantId<\/code> in the API layer.<\/li> \n<\/ol>\n<p>Sounds simple, doesn&#8217;t it? \ud83e\udd37<\/p>\n<img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-oss.png?resize=640%2C83&#038;ssl=1\" alt=\"\" width=\"640\" height=\"83\" class=\"alignnone size-large wp-image-1791\" srcset=\"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-oss.png?resize=1024%2C132&amp;ssl=1 1024w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-oss.png?resize=300%2C39&amp;ssl=1 300w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-oss.png?resize=768%2C99&amp;ssl=1 768w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-oss.png?resize=1536%2C198&amp;ssl=1 1536w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-oss.png?resize=604%2C78&amp;ssl=1 604w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-oss.png?w=1727&amp;ssl=1 1727w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-oss.png?w=1280&amp;ssl=1 1280w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/>\n<img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-ee-1024x132.png?resize=640%2C83&#038;ssl=1\" alt=\"\" width=\"640\" height=\"83\" class=\"alignnone size-large wp-image-1792\" srcset=\"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-ee.png?resize=1024%2C132&amp;ssl=1 1024w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-ee.png?resize=300%2C39&amp;ssl=1 300w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-ee.png?resize=768%2C99&amp;ssl=1 768w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-ee.png?resize=1536%2C198&amp;ssl=1 1536w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-ee.png?resize=604%2C78&amp;ssl=1 604w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-ee.png?w=1727&amp;ssl=1 1727w, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/pr-ee.png?w=1280&amp;ssl=1 1280w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/>\n<p>A plan always goes\n<del>without<\/del> with hiccups!<\/p>\n<p>Adding a property to a large number of classes \/ filters \/ queries \/ &#8230; brings a high risk of oversight, and therefore of bugs. And that&#8217;s exactly what happened. Despite great care during implementation, there were a few places where the tenant was not passed (mainly due to the use of Lombok builders &#8230; I won&#8217;t dwell into it). Similarly, some parts of Kestra require knowledge of all data (for example, all flows), so we had to ensure that, for example, when we list flows from the API, the list is filtered by tenant, but not when we list flows from the flow scheduler.<\/p>\n<p>To facilitate the migration of our existing users, we allowed the use of a default tenant, which is the tenant whose identifier is null. This was the cause of many other bugs&#8230;<\/p>\n<p>In conclusion, multitenancy is essential when setting up a SaaS, and once you&#8217;ve carefully chosen your implementation model, you can expect a long, laborious and bug-prone implementation. To mitigate the risk of bugs, we chose to merge the PR on the multi-tenant at the start of the release cycle, which enabled us to test it for a month on our own test environments before delivering it to our users, thus uncovering many of the bugs that had been introduced. I strongly recommend that you plan a substantial testing period, as we did.<\/p>\n<p>The final word: implementing a multi-tenant architecture isn&#8217;t easy, idealy, it needs to be implemented as early as possible in a code base.<\/p>","protected":false},"excerpt":{"rendered":"<p>This article is based on my talk Designing a multi-tenant SaaS given at Cloud Nord on October 12, 2023. Kestra is a highly scalable data scheduling and orchestration platform that creates, executes, schedules and monitors millions of complex pipelines. For an introduction to Kestra, you can read my article on the subject. One of the recent Kestra evolutions I was responsible for was multitenancy support, this article will tell you about the design that went into adding this functionality. Multitenancy&#8230;<p class=\"read-more\"><a class=\"btn btn-default\" href=\"https:\/\/www.loicmathieu.fr\/wordpress\/informatique\/concevoir-un-saas-multitenant\/\"> Read More<span class=\"screen-reader-text\">  Read More<\/span><\/a><\/p><\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"activitypub_content_warning":"","activitypub_content_visibility":"","activitypub_max_image_attachments":4,"activitypub_interaction_policy_quote":"anyone","activitypub_status":"","footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[9],"tags":[],"class_list":["post-1786","post","type-post","status-publish","format-standard","hentry","category-informatique"],"aioseo_notices":[],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"jetpack-related-posts":[{"id":1606,"url":"https:\/\/www.loicmathieu.fr\/wordpress\/informatique\/au-revoir-zenika-bonjour-kestra\/","url_meta":{"origin":1786,"position":0},"title":"(Fran\u00e7ais) Au revoir Zenika, bonjour Kestra","author":"admin","date":"Tuesday January 10th, 2023","format":false,"excerpt":"Sorry, this entry is only available in Fran\u00e7ais.","rel":"","context":"In &quot;informatique&quot;","block_context":{"text":"informatique","link":"https:\/\/www.loicmathieu.fr\/wordpress\/category\/informatique\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1611,"url":"https:\/\/www.loicmathieu.fr\/wordpress\/informatique\/introduction-a-kestra\/","url_meta":{"origin":1786,"position":1},"title":"Introduction to Kestra","author":"admin","date":"Monday March  6th, 2023","format":false,"excerpt":"Kestra is an open-source data orchestrator and scheduler. With Kestra, data workflows, called flows, use the YAML format and are executed by its engine via an API call, the user interface, or a trigger (webhook, schedule, SQL query, Pub\/Sub message, ...). The important notions of Kestra are : The flow:\u2026","rel":"","context":"In &quot;informatique&quot;","block_context":{"text":"informatique","link":"https:\/\/www.loicmathieu.fr\/wordpress\/category\/informatique\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/kestra-01-1024x267.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/kestra-01-1024x267.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/kestra-01-1024x267.png?resize=525%2C300&ssl=1 1.5x"},"classes":[]},{"id":1650,"url":"https:\/\/www.loicmathieu.fr\/wordpress\/informatique\/le-profiler-sql-de-visualvm\/","url_meta":{"origin":1786,"position":2},"title":"VisualVM SQL profiler SQL","author":"admin","date":"Tuesday April  4th, 2023","format":false,"excerpt":"A little while ago, I discovered the SQL profiler of VisualVM and I thought I should share it with you ;). VisualVM is a tool that provides a visual interface to display detailed information about applications running on a Java Virtual Machine (JVM). VisualVM is designed for use in development\u2026","rel":"","context":"In &quot;informatique&quot;","block_context":{"text":"informatique","link":"https:\/\/www.loicmathieu.fr\/wordpress\/category\/informatique\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/Capture-decran-du-2023-04-03-14-17-25-1024x624.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/Capture-decran-du-2023-04-03-14-17-25-1024x624.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/Capture-decran-du-2023-04-03-14-17-25-1024x624.png?resize=525%2C300&ssl=1 1.5x"},"classes":[]},{"id":1847,"url":"https:\/\/www.loicmathieu.fr\/wordpress\/informatique\/jooq-tip-ne-convertissez-pas-jsonb-en-string\/","url_meta":{"origin":1786,"position":3},"title":"jOOQ tip: don&#8217;t convert JSONB to a String","author":"admin","date":"Wednesday October 23rd, 2024","format":false,"excerpt":"A few weeks ago, while investigating possible performance improvements for Kestra's JDBC backend, I noticed that a method we were using to map an entity to be persisted in the database into its JSONB representation was taking up a lot of time in our CPU profiles. In the following flame\u2026","rel":"","context":"In &quot;informatique&quot;","block_context":{"text":"informatique","link":"https:\/\/www.loicmathieu.fr\/wordpress\/category\/informatique\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/366507373-c44206a5-0085-43cc-902e-97756319b0ea-1024x737.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/366507373-c44206a5-0085-43cc-902e-97756319b0ea-1024x737.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/loicmathieu.fr\/wordpress\/wp-content\/uploads\/366507373-c44206a5-0085-43cc-902e-97756319b0ea-1024x737.png?resize=525%2C300&ssl=1 1.5x"},"classes":[]},{"id":2007,"url":"https:\/\/www.loicmathieu.fr\/wordpress\/informatique\/characterescapes-jackson-s-hidden-gem\/","url_meta":{"origin":1786,"position":4},"title":"CharacterEscapes: Jackson&#8217;s hidden gem","author":"admin","date":"Wednesday September 10th, 2025","format":false,"excerpt":"At Kestra, the data orchestration platform I work for, we had an issue ([#10326] (https:\/\/github.com\/kestra-io\/kestra\/issues\/10326)) opened by a user reporting a problem with the PostgreSQL database and the Unicode character \\u0000. A workflow task that returned this character in its output was failing. After investigation, PostgreSQL refuses to store a\u2026","rel":"","context":"In &quot;informatique&quot;","block_context":{"text":"informatique","link":"https:\/\/www.loicmathieu.fr\/wordpress\/category\/informatique\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":966,"url":"https:\/\/www.loicmathieu.fr\/wordpress\/informatique\/1-an-chez-zenika\/","url_meta":{"origin":1786,"position":5},"title":"(Fran\u00e7ais) 1 an chez Zenika","author":"admin","date":"Tuesday September  3rd, 2019","format":false,"excerpt":"Sorry, this entry is only available in Fran\u00e7ais.","rel":"","context":"In &quot;informatique&quot;","block_context":{"text":"informatique","link":"https:\/\/www.loicmathieu.fr\/wordpress\/category\/informatique\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"_links":{"self":[{"href":"https:\/\/www.loicmathieu.fr\/wordpress\/wp-json\/wp\/v2\/posts\/1786","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.loicmathieu.fr\/wordpress\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.loicmathieu.fr\/wordpress\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.loicmathieu.fr\/wordpress\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.loicmathieu.fr\/wordpress\/wp-json\/wp\/v2\/comments?post=1786"}],"version-history":[{"count":15,"href":"https:\/\/www.loicmathieu.fr\/wordpress\/wp-json\/wp\/v2\/posts\/1786\/revisions"}],"predecessor-version":[{"id":1804,"href":"https:\/\/www.loicmathieu.fr\/wordpress\/wp-json\/wp\/v2\/posts\/1786\/revisions\/1804"}],"wp:attachment":[{"href":"https:\/\/www.loicmathieu.fr\/wordpress\/wp-json\/wp\/v2\/media?parent=1786"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.loicmathieu.fr\/wordpress\/wp-json\/wp\/v2\/categories?post=1786"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.loicmathieu.fr\/wordpress\/wp-json\/wp\/v2\/tags?post=1786"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}