diff --git a/backends/key-file/kf-backend.vala b/backends/key-file/kf-backend.vala
index 1fd3961..0e4319a 100644
--- a/backends/key-file/kf-backend.vala
+++ b/backends/key-file/kf-backend.vala
@@ -52,6 +52,9 @@ public class Folks.Backends.Kf.Backend : Folks.Backend
 
   public override async void prepare ()
     {
+      if (this.is_prepared)
+        return;
+
       File file;
       string path = Environment.get_variable ("FOLKS_BACKEND_KEY_FILE_PATH");
       if (path == null)
@@ -77,6 +80,8 @@ public class Folks.Backends.Kf.Backend : Folks.Backend
       store.removed.connect (this.store_removed_cb);
 
       this.persona_store_added (store);
+
+      this.is_prepared = true;
     }
 
   private void store_removed_cb (Folks.PersonaStore store)
diff --git a/backends/key-file/kf-persona-store.vala b/backends/key-file/kf-persona-store.vala
index 153810e..d215239 100644
--- a/backends/key-file/kf-persona-store.vala
+++ b/backends/key-file/kf-persona-store.vala
@@ -76,6 +76,9 @@ public class Folks.Backends.Kf.PersonaStore : Folks.PersonaStore
 
   public override async void prepare ()
     {
+      if (this.is_prepared)
+        return;
+
       string filename = this.file.get_path ();
       this.key_file = new GLib.KeyFile ();
 
@@ -169,6 +172,8 @@ public class Folks.Backends.Kf.PersonaStore : Folks.PersonaStore
           this.personas_changed (this._personas.get_values (), null, null, null,
               Groups.ChangeReason.NONE);
         }
+
+      this.is_prepared = true;
     }
 
   /**
diff --git a/backends/telepathy/lib/tpf-persona-store.vala b/backends/telepathy/lib/tpf-persona-store.vala
index 3e9cf4b..5b9fe8d 100644
--- a/backends/telepathy/lib/tpf-persona-store.vala
+++ b/backends/telepathy/lib/tpf-persona-store.vala
@@ -142,6 +142,9 @@ public class Tpf.PersonaStore : Folks.PersonaStore
 
   public override async void prepare ()
     {
+      if (this.is_prepared)
+        return;
+
       this.account_manager = AccountManager.dup ();
 
       this.account_manager.account_disabled.connect ((a) =>
@@ -201,6 +204,8 @@ public class Tpf.PersonaStore : Folks.PersonaStore
           warning ("couldn't connect to the telepathy-logger service");
           this.logger = null;
         }
+
+      this.is_prepared = true;
     }
 
   private async void initialise_favourite_contacts ()
diff --git a/backends/telepathy/tp-backend.vala b/backends/telepathy/tp-backend.vala
index c5dc516..cd4cb73 100644
--- a/backends/telepathy/tp-backend.vala
+++ b/backends/telepathy/tp-backend.vala
@@ -54,6 +54,9 @@ public class Folks.Backends.Tp.Backend : Folks.Backend
 
   public override async void prepare () throws GLib.Error
     {
+      if (this.is_prepared)
+        return;
+
       this.account_manager = AccountManager.dup ();
       yield this.account_manager.prepare_async (null);
       this.account_manager.account_enabled.connect (this.account_enabled_cb);
@@ -68,6 +71,8 @@ public class Folks.Backends.Tp.Backend : Folks.Backend
         {
           this.account_enabled_cb (account);
         }
+
+      this.is_prepared = true;
     }
 
   private void account_enabled_cb (Account account)
diff --git a/folks/backend-store.vala b/folks/backend-store.vala
index d74848c..d1998e6 100644
--- a/folks/backend-store.vala
+++ b/folks/backend-store.vala
@@ -133,26 +133,30 @@ public class Folks.BackendStore : Object {
    * searched for in a path set at compilation time.
    */
   public async void load_backends () throws GLib.Error {
-      assert (Module.supported());
-
-      var path = Environment.get_variable ("FOLKS_BACKEND_DIR");
-      if (path == null)
+      if (!this.backends_loaded)
         {
-          path = BuildConf.BACKEND_DIR;
+          assert (Module.supported());
 
-          debug ("Using built-in backend dir '%s' (override with environment "
-              + "variable FOLKS_BACKEND_DIR)", path);
-        }
-      else
-        {
-          debug ("Using environment variable FOLKS_BACKEND_DIR = '%s' to look "
-              + "for backends", path);
-        }
+          var path = Environment.get_variable ("FOLKS_BACKEND_DIR");
+          if (path == null)
+            {
+              path = BuildConf.BACKEND_DIR;
 
-      File dir = File.new_for_path (path);
-      assert (dir != null && yield is_dir (dir));
+              debug ("Using built-in backend dir '%s' (override with environment "
+                  + "variable FOLKS_BACKEND_DIR)", path);
+            }
+          else
+            {
+              debug ("Using environment variable FOLKS_BACKEND_DIR = '%s' to look "
+                  + "for backends", path);
+            }
 
-      yield this.load_modules_from_dir (dir);
+          File dir = File.new_for_path (path);
+          assert (dir != null && yield is_dir (dir));
+
+          yield this.load_modules_from_dir (dir);
+          this.backends_loaded = true;
+        }
   }
 
   /**
diff --git a/folks/backend.vala b/folks/backend.vala
index 0bdb419..1939108 100644
--- a/folks/backend.vala
+++ b/folks/backend.vala
@@ -36,6 +36,14 @@ using Folks;
 public abstract class Folks.Backend : Object
 {
   /**
+   * Whether {@link Backend.prepare} has successfully completed for this
+   * backend.
+   *
+   * @since 0.1.17
+   */
+  public bool is_prepared { get; protected set; default = false; }
+
+  /**
    * A unique name for the backend.
    *
    * This will be used to identify the backend, and should also be used as the
@@ -91,6 +99,8 @@ public abstract class Folks.Backend : Object
    * This is normally handled transparently by the {@link IndividualAggregator}.
    *
    * If this function throws an error, the Backend will not be functional.
+   *
+   * This function is guaranteed to be idempotent (since version 0.1.17).
    */
   public abstract async void prepare () throws GLib.Error;
 
diff --git a/folks/individual-aggregator.vala b/folks/individual-aggregator.vala
index 393644f..0cdcc17 100644
--- a/folks/individual-aggregator.vala
+++ b/folks/individual-aggregator.vala
@@ -59,6 +59,14 @@ public class Folks.IndividualAggregator : Object
   private bool linking_enabled = true;
 
   /**
+   * Whether {@link IndividualAggregator.prepare} has successfully completed for
+   * this aggregator.
+   *
+   * @since 0.1.17
+   */
+  public bool is_prepared { get; private set; default = false; }
+
+  /**
    * A table mapping {@link Individual.id}s to their {@link Individual}s.
    *
    * This is the canonical set of {@link Individual}s provided by this
@@ -140,10 +148,17 @@ public class Folks.IndividualAggregator : Object
    * function if you're using this from C), all the {@link Backend}s will have
    * been prepared (though no {@link PersonaStore}s are guaranteed to be
    * available yet). This last guarantee is new as of version 0.1.17.
+   *
+   * This function is guaranteed to be idempotent (since version 0.1.17).
    */
   public async void prepare () throws GLib.Error
     {
+      if (this.is_prepared)
+        return;
+
       yield this.backend_store.load_backends ();
+
+      this.is_prepared = true;
     }
 
   private async void prepare_backend (Backend backend)
diff --git a/folks/persona-store.vala b/folks/persona-store.vala
index 3e01eaa..408ae37 100644
--- a/folks/persona-store.vala
+++ b/folks/persona-store.vala
@@ -153,6 +153,14 @@ public abstract class Folks.PersonaStore : Object
    */
   public abstract HashTable<string, Persona> personas { get; }
 
+  /**
+   * Whether {@link PersonaStore.prepare} has successfully completed for this
+   * store.
+   *
+   * @since 0.1.17
+   */
+  public bool is_prepared { get; protected set; default = false; }
+
    /**
    * Whether the PersonaStore is writeable.
    *
@@ -190,6 +198,8 @@ public abstract class Folks.PersonaStore : Object
    * This is normally handled transparently by the {@link IndividualAggregator}.
    *
    * If this function throws an error, the PersonaStore will not be functional.
+   *
+   * This function is guaranteed to be idempotent (since version 0.1.17).
    */
   public abstract async void prepare () throws GLib.Error;
 
