diff --git a/arbor/arbexcept.cpp b/arbor/arbexcept.cpp
index 0a3a382c7fd0cb2e36c3c54d798c3a4659347aa7..76dfdd580e2328f128832ba45aaf145b1afe784d 100644
--- a/arbor/arbexcept.cpp
+++ b/arbor/arbexcept.cpp
@@ -69,9 +69,18 @@ invalid_parameter_value::invalid_parameter_value(const std::string& mech_name, c
     arbor_exception(pprintf("invalid parameter value for mechanism {} parameter {}: {}", mech_name, param_name, value)),
     mech_name(mech_name),
     param_name(param_name),
+    value_str(),
     value(value)
 {}
 
+invalid_parameter_value::invalid_parameter_value(const std::string& mech_name, const std::string& param_name, const std::string& value_str):
+    arbor_exception(pprintf("invalid parameter value for mechanism {} parameter {}: {}", mech_name, param_name, value_str)),
+    mech_name(mech_name),
+    param_name(param_name),
+    value_str(value_str),
+    value(0)
+{}
+
 invalid_ion_remap::invalid_ion_remap(const std::string& mech_name):
     arbor_exception(pprintf("invalid ion parameter remapping for mechanism {}", mech_name))
 {}
diff --git a/arbor/fvm_layout.cpp b/arbor/fvm_layout.cpp
index 4e021b2859f2c732da8118ed9c175d955b172134..c350770a8b911b53bc2f2eebfd48b3f4936b2f55 100644
--- a/arbor/fvm_layout.cpp
+++ b/arbor/fvm_layout.cpp
@@ -283,7 +283,7 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties&
     struct density_mech_data {
         std::vector<std::pair<size_type, const mechanism_desc*>> segments; //
         string_set paramset;
-        const mechanism_info* info = nullptr;
+        std::shared_ptr<mechanism_info> info = nullptr;
     };
     std::unordered_map<std::string, density_mech_data> density_mech_table;
 
@@ -300,7 +300,7 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties&
         };
         std::vector<point_data> points;
         string_set paramset;
-        const mechanism_info* info = nullptr;
+        std::shared_ptr<mechanism_info> info = nullptr;
     };
     std::unordered_map<std::string, point_mech_data> point_mech_table;
 
@@ -315,11 +315,11 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties&
 
     auto update_paramset_and_validate =
         [&catalogue]
-        (const mechanism_desc& desc, const mechanism_info*& info, string_set& paramset)
+        (const mechanism_desc& desc, std::shared_ptr<mechanism_info>& info, string_set& paramset)
     {
         auto& name = desc.name();
         if (!info) {
-            info = &catalogue[name];
+            info.reset(new mechanism_info(catalogue[name]));
         }
         for (const auto& pv: desc.values()) {
             if (!paramset.count(pv.first)) {
@@ -400,11 +400,11 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties&
     }
 
     for (auto& entry: density_mech_table) {
-        verify_ion_usage(entry.first, entry.second.info);
+        verify_ion_usage(entry.first, entry.second.info.get());
     }
 
     for (auto& entry: point_mech_table) {
-        verify_ion_usage(entry.first, entry.second.info);
+        verify_ion_usage(entry.first, entry.second.info.get());
     }
 
     // II. Build ion and mechanism configs.
@@ -470,7 +470,7 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties&
         fvm_mechanism_config& config = mechdata.mechanisms[name];
         config.kind = mechanismKind::density;
 
-        auto nparam = build_param_data(entry.second.paramset, entry.second.info);
+        auto nparam = build_param_data(entry.second.paramset, entry.second.info.get());
 
         // In order to properly account for partially overriden paramaters in CVs
         // that are shared between segments, we need to track not only the area-weighted
@@ -588,7 +588,7 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties&
         const std::string& name = entry.first;
         const auto& points = entry.second.points;
 
-        auto nparam = build_param_data(entry.second.paramset, entry.second.info);
+        auto nparam = build_param_data(entry.second.paramset, entry.second.info.get());
         std::vector<std::vector<value_type>> param_value(nparam);
 
         // Permute points in this mechanism so that they are in increasing CV order;
diff --git a/arbor/include/arbor/arbexcept.hpp b/arbor/include/arbor/arbexcept.hpp
index 4f7d0d4d27d83293823b30459042003f4e89938b..6eb4a4bfd8016ae65d3a48db1a7f361f02592e45 100644
--- a/arbor/include/arbor/arbexcept.hpp
+++ b/arbor/include/arbor/arbexcept.hpp
@@ -89,9 +89,11 @@ struct no_such_parameter: arbor_exception {
 };
 
 struct invalid_parameter_value: arbor_exception {
+    invalid_parameter_value(const std::string& mech_name, const std::string& param_name, const std::string& value_str);
     invalid_parameter_value(const std::string& mech_name, const std::string& param_name, double value);
     std::string mech_name;
     std::string param_name;
+    std::string value_str;
     double value;
 };
 
diff --git a/arbor/include/arbor/mechcat.hpp b/arbor/include/arbor/mechcat.hpp
index 76a03df4a5b10f980e608c21b2990a560efdf51c..bbe80e6e7ad6c89a1fe722c15311bda731b3478b 100644
--- a/arbor/include/arbor/mechcat.hpp
+++ b/arbor/include/arbor/mechcat.hpp
@@ -8,6 +8,7 @@
 
 #include <arbor/mechinfo.hpp>
 #include <arbor/mechanism.hpp>
+#include <arbor/util/optional.hpp>
 
 // Mechanism catalogue maintains:
 //
@@ -28,39 +29,41 @@
 // There is in addition a global default mechanism catalogue object that is
 // populated with any builtin mechanisms and mechanisms generated from
 // module files included with arbor.
+//
+// When a mechanism name of the form "mech/param=value,..." is requested, if the
+// mechanism of that name does not already exist in the catalogue, it will be
+// implicitly derived from an existing mechanism "mech", with global parameters
+// and ion bindings overridden by the supplied assignments that follow the slash.
+// If the mechanism in question has a single ion dependence, then that ion name
+// can be omitted in the assignments; "mech/oldion=newion" will make the same
+// derived mechanism as simply "mech/newion".
 
 namespace arb {
 
+// catalogue_state comprises the private implementation of mechanism_catalogue.
+struct catalogue_state;
+
 class mechanism_catalogue {
 public:
     using value_type = double;
 
-    mechanism_catalogue() = default;
-    mechanism_catalogue(mechanism_catalogue&& other) = default;
-    mechanism_catalogue& operator=(mechanism_catalogue&& other) = default;
-
-    // Copying a catalogue requires cloning the prototypes.
-    mechanism_catalogue(const mechanism_catalogue& other) {
-        copy_impl(other);
-    }
+    mechanism_catalogue();
+    mechanism_catalogue(mechanism_catalogue&& other);
+    mechanism_catalogue& operator=(mechanism_catalogue&& other);
 
-    mechanism_catalogue& operator=(const mechanism_catalogue& other) {
-        copy_impl(other);
-        return *this;
-    }
+    mechanism_catalogue(const mechanism_catalogue& other);
+    mechanism_catalogue& operator=(const mechanism_catalogue& other);
 
     void add(const std::string& name, mechanism_info info);
 
-    bool has(const std::string& name) const {
-        return info_map_.count(name) || is_derived(name);
-    }
+    // Has `name` been added, derived, or can it be implicitly derived?
+    bool has(const std::string& name) const;
 
-    bool is_derived(const std::string& name) const {
-        return derived_map_.count(name);
-    }
+    // Is `name` a derived mechanism or can it be implicitly derived?
+    bool is_derived(const std::string& name) const;
 
     // Read-only access to mechanism info.
-    const mechanism_info& operator[](const std::string& name) const;
+    mechanism_info operator[](const std::string& name) const;
 
     // Read-only access to mechanism fingerprint.
     const mechanism_fingerprint& fingerprint(const std::string& name) const;
@@ -72,7 +75,7 @@ public:
                 const std::vector<std::pair<std::string, double>>& global_params,
                 const std::vector<std::pair<std::string, std::string>>& ion_remap = {});
 
-    // Remove mechanism from catalogue, together with any derived.
+    // Remove mechanism from catalogue, together with any derivations of it.
     void remove(const std::string& name);
 
     // Clone the implementation associated with name (search derivation hierarchy starting from
@@ -101,40 +104,15 @@ public:
         register_impl(std::type_index(typeid(B)), name, std::move(generic_proto));
     }
 
-private:
-    using mechanism_info_ptr = std::unique_ptr<mechanism_info>;
-
-    template <typename V>
-    using string_map = std::unordered_map<std::string, V>;
-
-    // Schemata for (un-derived) mechanisms.
-    string_map<mechanism_info_ptr> info_map_;
-
-    struct derivation {
-        std::string parent;
-        string_map<value_type> globals;    // global overrides relative to parent
-        string_map<std::string> ion_remap; // ion name remap overrides relative to parent
-        mechanism_info_ptr derived_info;
-    };
-
-    // Parent and global setting values for derived mechanisms.
-    string_map<derivation> derived_map_;
+    ~mechanism_catalogue();
 
-    // Prototype register, keyed on mechanism name, then backend type (index).
-    string_map<std::unordered_map<std::type_index, mechanism_ptr>> impl_map_;
+private:
+    std::unique_ptr<catalogue_state> state_;
 
-    // Concrete-type erased helper methods.
     std::pair<mechanism_ptr, mechanism_overrides> instance_impl(std::type_index, const std::string&) const;
     void register_impl(std::type_index, const std::string&, mechanism_ptr);
-
-    // Perform copy and prototype clone from other catalogue (overwrites all entries).
-    void copy_impl(const mechanism_catalogue&);
 };
 
-// Convenience routine for deriving a single-ion dependency mechanism 'name'
-// over a new ion 'ion', and adding it to the catalogue as 'name/ion'.
-
-void parameterize_over_ion(mechanism_catalogue&, const std::string& name, const std::string& ion);
 
 // Reference to global default mechanism catalogue.
 
diff --git a/arbor/include/arbor/simulation.hpp b/arbor/include/arbor/simulation.hpp
index bff327d1d4c2cbc2cb439eec843cb79bb1eda26b..3dc177a4467455fb8d1c7ee2c7b2cad6afdda4e0 100644
--- a/arbor/include/arbor/simulation.hpp
+++ b/arbor/include/arbor/simulation.hpp
@@ -18,7 +18,7 @@ namespace arb {
 
 using spike_export_function = std::function<void(const std::vector<spike>&)>;
 
-// simulation_state comprises private implentation for simulation class.
+// simulation_state comprises private implementation for simulation class.
 class simulation_state;
 
 class simulation {
diff --git a/arbor/mechcat.cpp b/arbor/mechcat.cpp
index af1eb807e5b3d04bd974255078303f3d04b91f40..05408d157cf1244e0620f3a7fa0b1e883570f333 100644
--- a/arbor/mechcat.cpp
+++ b/arbor/mechcat.cpp
@@ -1,3 +1,4 @@
+#include <cstdlib>
 #include <map>
 #include <memory>
 #include <string>
@@ -6,6 +7,7 @@
 #include <arbor/arbexcept.hpp>
 #include <arbor/mechcat.hpp>
 
+#include "util/either.hpp"
 #include "util/maputil.hpp"
 
 /* Notes on implementation:
@@ -16,7 +18,7 @@
  *
  *    This contains the mapping between mechanism names and concrete mechanisms
  *    for a specific backend that have been registered with
- *    register_implementation().
+ *    register_impl().
  *
  *    It is a two-level map, first indexed by name, and then by the back-end
  *    type (using std::type_index).
@@ -49,245 +51,510 @@
  * global parameter and ion overrides that need to be applied, starting from
  * the top-most (least-derived) ancestor and working down to the requested derived
  * mechanism.
+ *
+ * The private implementation class catalogue_state does not throw any (catalogue
+ * related) exceptions, but instead propagates errors via util::either to the
+ * mechanism_catalogue methods for handling.
  */
 
 namespace arb {
 
 using util::value_by_key;
+using util::optional;
+using util::nullopt;
+
 using std::make_unique;
+using std::make_exception_ptr;
 
-void mechanism_catalogue::add(const std::string& name, mechanism_info info) {
-    if (has(name)) {
-        throw duplicate_mechanism(name);
-    }
+using mechanism_info_ptr = std::unique_ptr<mechanism_info>;
 
-    info_map_[name] = mechanism_info_ptr(new mechanism_info(std::move(info)));
-}
+template <typename V>
+using string_map = std::unordered_map<std::string, V>;
 
-const mechanism_info& mechanism_catalogue::operator[](const std::string& name) const {
-    if (const auto& deriv = value_by_key(derived_map_, name)) {
-        return *(deriv->derived_info.get());
-    }
-    else if (auto p = value_by_key(info_map_, name)) {
-        return *(p->get());
-    }
+template <typename T>
+struct hopefully_typemap {
+    using type = util::either<T, std::exception_ptr>;
+};
 
-    throw no_such_mechanism(name);
-}
+template <>
+struct hopefully_typemap<void> {
+    struct placeholder_type {};
+    using type = util::either<placeholder_type, std::exception_ptr>;
+};
 
-const mechanism_fingerprint& mechanism_catalogue::fingerprint(const std::string& name) const {
-    std::string base = name;
-    while (auto deriv = value_by_key(derived_map_, base)) {
-        base = deriv->parent;
+template <typename T>
+using hopefully = typename hopefully_typemap<T>::type;
+
+// Convert hopefully<T> to T or throw.
+
+template <typename T>
+const T& value(const util::either<T, std::exception_ptr>& x) {
+    if (!x) {
+        std::rethrow_exception(x.second());
     }
+    return x.first();
+}
 
-    if (const auto& p = value_by_key(info_map_, base)) {
-        return p.value()->fingerprint;
+template <typename T>
+T value(util::either<T, std::exception_ptr>&& x) {
+    if (!x) {
+        std::rethrow_exception(x.second());
     }
+    return std::move(x.first());
+}
 
-    throw no_such_mechanism(name);
+void value(const hopefully<void>& x) {
+    if (!x) {
+        std::rethrow_exception(x.second());
+    }
 }
 
-void mechanism_catalogue::derive(const std::string& name, const std::string& parent,
-    const std::vector<std::pair<std::string, double>>& global_params,
-    const std::vector<std::pair<std::string, std::string>>& ion_remap_vec)
-{
-    if (has(name)) {
-        throw duplicate_mechanism(name);
+struct derivation {
+    std::string parent;
+    string_map<double> globals;        // global overrides relative to parent
+    string_map<std::string> ion_remap; // ion name remap overrides relative to parent
+    mechanism_info_ptr derived_info;
+};
+
+
+// (Pimpl) catalogue state.
+
+struct catalogue_state {
+    catalogue_state() = default;
+
+    catalogue_state(const catalogue_state& other) {
+        info_map_.clear();
+        for (const auto& kv: other.info_map_) {
+            info_map_[kv.first] = make_unique<mechanism_info>(*kv.second);
+        }
+
+        derived_map_.clear();
+        for (const auto& kv: other.derived_map_) {
+            const derivation& v = kv.second;
+            derived_map_[kv.first] = {v.parent, v.globals, v.ion_remap, make_unique<mechanism_info>(*v.derived_info)};
+        }
+
+        impl_map_.clear();
+        for (const auto& name_impls: other.impl_map_) {
+            std::unordered_map<std::type_index, std::unique_ptr<mechanism>> impls;
+            for (const auto& tidx_mptr: name_impls.second) {
+                impls[tidx_mptr.first] = tidx_mptr.second->clone();
+            }
+
+            impl_map_[name_impls.first] = std::move(impls);
+        }
     }
 
-    if (!has(parent)) {
-        throw no_such_mechanism(parent);
+    // Check for presence of mechanism or derived mechanism.
+    bool defined(const std::string& name) const {
+        return info_map_.count(name) || derived_map_.count(name);
     }
 
-    string_map<std::string> ion_remap_map(ion_remap_vec.begin(), ion_remap_vec.end());
-    derivation deriv = {parent, {}, ion_remap_map, nullptr};
-    mechanism_info_ptr info = mechanism_info_ptr(new mechanism_info((*this)[deriv.parent]));
+    // Check if name is derived or implicitly derivable.
+    bool is_derived(const std::string& name) const {
+        return derived_map_.count(name) || derive(name);
+    }
 
-    // Update global parameter values in info for derived mechanism.
+    // Set mechanism info (unchecked).
+    void bind(const std::string& name, mechanism_info info) {
+        info_map_[name] = mechanism_info_ptr(new mechanism_info(std::move(info)));
+    }
 
-    for (const auto& kv: global_params) {
-        const auto& param = kv.first;
-        const auto& value = kv.second;
+    // Add derived mechanism (unchecked).
+    void bind(const std::string& name, derivation deriv) {
+        derived_map_[name] = std::move(deriv);
+    }
 
-        if (auto p = value_by_key(info->globals, param)) {
-            if (!p->valid(value)) {
-                throw invalid_parameter_value(name, param, value);
+    // Register concrete mechanism for a back-end type.
+    hopefully<void> register_impl(std::type_index tidx, const std::string& name, std::unique_ptr<mechanism> mech) {
+        if (auto fptr = fingerprint_ptr(name)) {
+            if (mech->fingerprint()!=*fptr.first()) {
+                return make_exception_ptr(fingerprint_mismatch(name));
             }
+
+            impl_map_[name][tidx] = std::move(mech);
         }
         else {
-            throw no_such_parameter(name, param);
+            return fptr.second();
         }
 
-        deriv.globals[param] = value;
-        info->globals.at(param).default_value = value;
+        return {};
     }
 
-    for (const auto& kv: ion_remap_vec) {
-        if (!info->ions.count(kv.first)) {
-            throw invalid_ion_remap(name, kv.first, kv.second);
+    // Remove mechanism and its derivations and implementations.
+    void remove(const std::string& name) {
+        derived_map_.erase(name);
+        info_map_.erase(name);
+        impl_map_.erase(name);
+
+        // Erase any dangling derivation map entries.
+        std::size_t n_delete;
+        do {
+            n_delete = 0;
+            for (auto it = derived_map_.begin(); it!=derived_map_.end(); ) {
+                const auto& parent = it->second.parent;
+                if (info_map_.count(parent) || derived_map_.count(parent)) {
+                    ++it;
+                }
+                else {
+                    impl_map_.erase(it->first);
+                    derived_map_.erase(it++);
+                    ++n_delete;
+                }
+            }
+        } while (n_delete>0);
+    }
+
+    // Retrieve mechanism info for mechanism, derived mechanism, or implicitly
+    // derived mechanism.
+    hopefully<mechanism_info> info(const std::string& name) const {
+        if (const auto& deriv = value_by_key(derived_map_, name)) {
+            return *(deriv->derived_info.get());
+        }
+        else if (auto p = value_by_key(info_map_, name)) {
+            return *(p->get());
+        }
+        else if (auto deriv = derive(name)) {
+            return *(deriv.first().derived_info.get());
+        }
+        else {
+            return deriv.second();
         }
     }
 
-    // Update ion dependencies in info to reflect the requested ion remapping.
+    // Retrieve mechanism fingerprint. The fingerprint of a derived mechanisms
+    // is that of its parent.
+    hopefully<const mechanism_fingerprint*> fingerprint_ptr(const std::string& name) const {
+        hopefully<derivation> implicit_deriv;
+        const std::string* base = &name;
 
-    string_map<ion_dependency> new_ions;
-    for (const auto& kv: info->ions) {
-        if (auto new_ion = value_by_key(ion_remap_map, kv.first)) {
-            if (!new_ions.insert({*new_ion, kv.second}).second) {
-                throw invalid_ion_remap(name, kv.first, *new_ion);
+        if (!defined(name)) {
+            if (implicit_deriv = derive(name)) {
+                base = &implicit_deriv.first().parent;
+            }
+            else {
+                return implicit_deriv.second();
             }
         }
+
+        while (auto maybe_deriv = value_by_key(derived_map_, *base)) {
+            base = &maybe_deriv->parent;
+        }
+
+        if (const auto& p = value_by_key(info_map_, *base)) {
+            return &p.value()->fingerprint;
+        }
+
+        throw arbor_internal_error("inconsistent catalogue map state");
+    }
+
+    // Construct derived mechanism based on existing parent mechanism and overrides.
+    hopefully<derivation> derive(
+        const std::string& name, const std::string& parent,
+        const std::vector<std::pair<std::string, double>>& global_params,
+        const std::vector<std::pair<std::string, std::string>>& ion_remap_vec) const
+    {
+        if (defined(name)) {
+            return make_exception_ptr(duplicate_mechanism(name));
+        }
+        else if (!defined(parent)) {
+            return make_exception_ptr(no_such_mechanism(parent));
+        }
+
+        string_map<std::string> ion_remap_map(ion_remap_vec.begin(), ion_remap_vec.end());
+        derivation deriv = {parent, {}, ion_remap_map, nullptr};
+
+        mechanism_info_ptr new_info;
+        if (auto parent_info = info(parent)) {
+            new_info.reset(new mechanism_info(parent_info.first()));
+        }
         else {
-            if (!new_ions.insert(kv).second) {
-                // (find offending remap to report in exception)
-                for (const auto& entry: ion_remap_map) {
-                    if (entry.second==kv.first) {
-                        throw invalid_ion_remap(name, kv.first, entry.second);
+            return parent_info.second();
+        }
+
+        // Update global parameter values in info for derived mechanism.
+
+        for (const auto& kv: global_params) {
+            const auto& param = kv.first;
+            const auto& value = kv.second;
+
+            if (auto p = value_by_key(new_info->globals, param)) {
+                if (!p->valid(value)) {
+                    return make_exception_ptr(invalid_parameter_value(name, param, value));
+                }
+            }
+            else {
+                return make_exception_ptr(no_such_parameter(name, param));
+            }
+
+            deriv.globals[param] = value;
+            new_info->globals.at(param).default_value = value;
+        }
+
+        for (const auto& kv: ion_remap_vec) {
+            if (!new_info->ions.count(kv.first)) {
+                return make_exception_ptr(invalid_ion_remap(name, kv.first, kv.second));
+            }
+        }
+
+        // Update ion dependencies in info to reflect the requested ion remapping.
+
+        string_map<ion_dependency> new_ions;
+        for (const auto& kv: new_info->ions) {
+            if (auto new_ion = value_by_key(ion_remap_map, kv.first)) {
+                if (!new_ions.insert({*new_ion, kv.second}).second) {
+                    return make_exception_ptr(invalid_ion_remap(name, kv.first, *new_ion));
+                }
+            }
+            else {
+                if (!new_ions.insert(kv).second) {
+                    // (find offending remap to report in exception)
+                    for (const auto& entry: ion_remap_map) {
+                        if (entry.second==kv.first) {
+                            return make_exception_ptr(invalid_ion_remap(name, kv.first, entry.second));
+                        }
                     }
+                    throw arbor_internal_error("inconsistent catalogue ion remap state");
                 }
-                throw arbor_internal_error("inconsistent catalogue ion remap state");
             }
         }
+        new_info->ions = std::move(new_ions);
+
+        deriv.derived_info = std::move(new_info);
+        return deriv;
     }
-    info->ions = std::move(new_ions);
 
-    deriv.derived_info = std::move(info);
-    derived_map_[name] = std::move(deriv);
-}
+    // Implicit derivation.
+    hopefully<derivation> derive(const std::string& name) const {
+        if (defined(name)) {
+            return make_exception_ptr(duplicate_mechanism(name));
+        }
 
-void mechanism_catalogue::remove(const std::string& name) {
-    if (!has(name)) {
-        throw no_such_mechanism(name);
-    }
+        auto i = name.find_last_of('/');
+        if (i==std::string::npos) {
+            return make_exception_ptr(no_such_mechanism(name));
+        }
 
-    if (is_derived(name)) {
-        derived_map_.erase(name);
-    }
-    else {
-        info_map_.erase(name);
-        impl_map_.erase(name);
-    }
+        std::string base = name.substr(0, i);
+        if (!defined(base)) {
+            return make_exception_ptr(no_such_mechanism(base));
+        }
 
-    // Erase any dangling derivation map entries.
-    std::size_t n_delete;
-    do {
-        n_delete = 0;
-        for (auto it = derived_map_.begin(); it!=derived_map_.end(); ) {
-            const auto& parent = it->second.parent;
-            if (info_map_.count(parent) || derived_map_.count(parent)) {
-                ++it;
+        std::string suffix = name.substr(i+1);
+
+        const mechanism_info_ptr& info = derived_map_.count(base)? derived_map_.at(base).derived_info: info_map_.at(base);
+        bool single_ion = info->ions.size()==1u;
+        auto is_ion = [&info](const std::string& name) -> bool { return info->ions.count(name); };
+
+        std::vector<std::pair<std::string, double>> global_params;
+        std::vector<std::pair<std::string, std::string>> ion_remap;
+
+        while (!suffix.empty()) {
+            std::string assign;
+
+            auto comma = suffix.find(',');
+            if (comma==std::string::npos) {
+                assign = suffix;
+                suffix.clear();
             }
             else {
-                derived_map_.erase(it++);
-                ++n_delete;
+                assign = suffix.substr(0, comma);
+                suffix = suffix.substr(comma+1);
             }
-        }
-    } while (n_delete>0);
-}
-
-std::pair<std::unique_ptr<mechanism>, mechanism_overrides>
-mechanism_catalogue::instance_impl(std::type_index tidx, const std::string& name) const {
-    std::pair<std::unique_ptr<mechanism>, mechanism_overrides> mech;
 
-    // Find implementation associated with this name or its closest ancestor.
+            std::string k, v;
+            auto eq = assign.find('=');
+            if (eq==std::string::npos) {
+                if (!single_ion) {
+                    return make_exception_ptr(invalid_ion_remap(assign));
+                }
 
-    auto impl_name = name;
-    const mechanism* prototype = nullptr;
+                k = info->ions.begin()->first;
+                v = assign;
+            }
+            else {
+                k = assign.substr(0, eq);
+                v = assign.substr(eq+1);
+            }
 
-    for (;;) {
-        if (const auto mech_impls = value_by_key(impl_map_, impl_name)) {
-            if (auto p = value_by_key(mech_impls.value(), tidx)) {
-                prototype = p->get();
-                break;
+            if (is_ion(k)) {
+                ion_remap.push_back({k, v});
+            }
+            else {
+                char* end = 0;
+                double v_value = std::strtod(v.c_str(), &end);
+                if (!end || *end) {
+                    return make_exception_ptr(invalid_parameter_value(name, k, v));
+                }
+                global_params.push_back({k, v_value});
             }
         }
 
-        // Try parent instead.
-        if (const auto p = value_by_key(derived_map_, impl_name)) {
-            impl_name = p->parent;
-        }
-        else {
-            throw no_such_implementation(name);
-        }
+        return derive(name, base, global_params, ion_remap);
     }
 
-    mech.first = prototype->clone();
+    // Retrieve implementation for this mechanism name or closest ancestor.
+    hopefully<std::unique_ptr<mechanism>> implementation(std::type_index tidx, const std::string& name) const {
+        const std::string* impl_name = &name;
+        hopefully<derivation> implicit_deriv;
+
+        if (!defined(name)) {
+            implicit_deriv = derive(name);
+            if (!implicit_deriv) {
+                return implicit_deriv.second();
+            }
+            impl_name = &implicit_deriv.first().parent;
+        }
+
+        for (;;) {
+            if (const auto mech_impls = value_by_key(impl_map_, *impl_name)) {
+                if (auto p = value_by_key(mech_impls.value(), tidx)) {
+                    return p->get()->clone();
+                }
+            }
 
-    // Recurse up the derivation tree to find the most distant ancestor;
-    // accumulate global parameter settings and ion remappings down to the
-    // requested mechanism.
+            // Try parent instead.
+            if (const auto p = value_by_key(derived_map_, *impl_name)) {
+                impl_name = &p->parent;
+            }
+            else {
+                return make_exception_ptr(no_such_implementation(name));
+            }
+        }
+    }
 
-    auto apply_globals = [this](auto& self, const std::string& name, mechanism_overrides& over) -> void {
-        if (auto p = value_by_key(derived_map_, name)) {
-            self(self, p->parent, over);
+    // Accumulate override set from derivation chain.
+    hopefully<mechanism_overrides> overrides(const std::string& name) const {
+        mechanism_overrides over;
 
-            for (auto& kv: p->globals) {
+        auto apply_deriv = [](mechanism_overrides& over, const derivation& deriv) {
+            for (auto& kv: deriv.globals) {
                 over.globals[kv.first] = kv.second;
             }
 
-            if (!p->ion_remap.empty()) {
-                string_map<std::string> new_rebind = p->ion_remap;
+            if (!deriv.ion_remap.empty()) {
+                string_map<std::string> new_rebind = deriv.ion_remap;
                 for (auto& kv: over.ion_rebind) {
-                    if (auto opt_v = value_by_key(p->ion_remap, kv.second)) {
+                    if (auto opt_v = value_by_key(deriv.ion_remap, kv.second)) {
                         new_rebind.erase(kv.second);
                         new_rebind[kv.first] = *opt_v;
                     }
                 }
                 for (auto& kv: over.ion_rebind) {
-                    if (!value_by_key(p->ion_remap, kv.second)) {
+                    if (!value_by_key(deriv.ion_remap, kv.second)) {
                         new_rebind[kv.first] = kv.second;
                     }
                 }
                 std::swap(new_rebind, over.ion_rebind);
             }
+        };
+
+        // Recurse up the derivation tree to find the most distant ancestor;
+        // accumulate global parameter settings and ion remappings down to the
+        // requested mechanism.
+
+        auto apply_globals = [this, &apply_deriv](auto& self, const std::string& name, mechanism_overrides& over) -> void {
+            if (auto p = value_by_key(derived_map_, name)) {
+                self(self, p->parent, over);
+                apply_deriv(over, *p);
+            }
+        };
+
+        util::optional<derivation> implicit_deriv;
+        if (!defined(name)) {
+            if (auto deriv = derive(name)) {
+                implicit_deriv = std::move(deriv.first());
+            }
+            else {
+                return deriv.second();
+            }
         }
-    };
-    apply_globals(apply_globals, name, mech.second);
-    return mech;
-}
 
-void mechanism_catalogue::register_impl(std::type_index tidx, const std::string& name, std::unique_ptr<mechanism> mech) {
-    const mechanism_info& info = (*this)[name];
+        apply_globals(apply_globals, implicit_deriv? implicit_deriv->parent: name, over);
+        if (implicit_deriv) {
+            apply_deriv(over, implicit_deriv.value());
+        }
 
-    if (mech->fingerprint()!=info.fingerprint) {
-        throw fingerprint_mismatch(name);
+        return over;
     }
 
-    impl_map_[name][tidx] = std::move(mech);
+    // Schemata for (un-derived) mechanisms.
+    string_map<mechanism_info_ptr> info_map_;
+
+    // Parent and global setting values for derived mechanisms.
+    string_map<derivation> derived_map_;
+
+    // Prototype register, keyed on mechanism name, then backend type (index).
+    string_map<std::unordered_map<std::type_index, mechanism_ptr>> impl_map_;
+};
+
+// Mechanism catalogue method implementations.
+
+mechanism_catalogue::mechanism_catalogue():
+    state_(new catalogue_state)
+{}
+
+mechanism_catalogue::mechanism_catalogue(mechanism_catalogue&& other) = default;
+mechanism_catalogue& mechanism_catalogue::operator=(mechanism_catalogue&& other) = default;
+
+mechanism_catalogue::mechanism_catalogue(const mechanism_catalogue& other):
+    state_(new catalogue_state(*other.state_))
+{}
+
+mechanism_catalogue& mechanism_catalogue::operator=(const mechanism_catalogue& other) {
+    state_.reset(new catalogue_state(*other.state_));
+    return *this;
 }
 
-void mechanism_catalogue::copy_impl(const mechanism_catalogue& other) {
-    info_map_.clear();
-    for (const auto& kv: other.info_map_) {
-        info_map_[kv.first] = make_unique<mechanism_info>(*kv.second);
+void mechanism_catalogue::add(const std::string& name, mechanism_info info) {
+    if (state_->defined(name)) {
+        throw duplicate_mechanism(name);
     }
+    state_->bind(name, std::move(info));
+}
 
-    derived_map_.clear();
-    for (const auto& kv: other.derived_map_) {
-        const derivation& v = kv.second;
-        derived_map_[kv.first] = {v.parent, v.globals, v.ion_remap, make_unique<mechanism_info>(*v.derived_info)};
-    }
+bool mechanism_catalogue::has(const std::string& name) const {
+    return state_->defined(name) || state_->derive(name);
+}
 
-    impl_map_.clear();
-    for (const auto& name_impls: other.impl_map_) {
-        std::unordered_map<std::type_index, std::unique_ptr<mechanism>> impls;
-        for (const auto& tidx_mptr: name_impls.second) {
-            impls[tidx_mptr.first] = tidx_mptr.second->clone();
-        }
+bool mechanism_catalogue::is_derived(const std::string& name) const {
+    return state_->is_derived(name);
+}
 
-        impl_map_[name_impls.first] = std::move(impls);
-    }
+mechanism_info mechanism_catalogue::operator[](const std::string& name) const {
+    return value(state_->info(name));
+}
+
+const mechanism_fingerprint& mechanism_catalogue::fingerprint(const std::string& name) const {
+    return *value(state_->fingerprint_ptr(name));
+}
+
+void mechanism_catalogue::derive(const std::string& name, const std::string& parent,
+    const std::vector<std::pair<std::string, double>>& global_params,
+    const std::vector<std::pair<std::string, std::string>>& ion_remap_vec)
+{
+    state_->bind(name, value(state_->derive(name, parent, global_params, ion_remap_vec)));
 }
 
-void parameterize_over_ion(mechanism_catalogue& cat, const std::string& name, const std::string& ion) {
-    mechanism_info info = cat[name];
-    if (info.ions.size()!=1) {
-        throw invalid_ion_remap(name);
+void mechanism_catalogue::remove(const std::string& name) {
+    if (!has(name)) {
+        throw no_such_mechanism(name);
     }
+    state_->remove(name);
+}
 
-    std::string from_ion = info.ions.begin()->first;
-    cat.derive(name+"/"+ion, name, {}, {{from_ion, ion}});
+void mechanism_catalogue::register_impl(std::type_index tidx, const std::string& name, std::unique_ptr<mechanism> mech) {
+    value(state_->register_impl(tidx, name, std::move(mech)));
 }
 
+std::pair<mechanism_ptr, mechanism_overrides> mechanism_catalogue::instance_impl(std::type_index tidx, const std::string& name) const {
+    std::pair<mechanism_ptr, mechanism_overrides> result;
+    result.first = value(state_->implementation(tidx, name));
+    result.second = value(state_->overrides(name));
+
+    return result;
+}
+
+mechanism_catalogue::~mechanism_catalogue() = default;
+
 } // namespace arb
diff --git a/test/unit/test_mechcat.cpp b/test/unit/test_mechcat.cpp
index db77676d517f9b234cdca64e8b065e8c3319ebf2..2d7ccac9432538b5f319bd9a4a30e2fe19945ab9 100644
--- a/test/unit/test_mechcat.cpp
+++ b/test/unit/test_mechcat.cpp
@@ -384,22 +384,51 @@ TEST(mechcat, bad_ion_rename) {
     EXPECT_THROW(cat.derive("alas", "fleeb", {}, {{"a", "b"}}), invalid_ion_remap);
 }
 
-TEST(mechcat, parameterize_over_ion) {
+TEST(mechcat, implicit_deriv) {
     auto cat = build_fake_catalogue();
 
-    // Can only use parametrize_over_ion with mechanisms that depend on exactly
-    // one ion.
-
-    EXPECT_THROW(parameterize_over_ion(cat, "fleeb", "nope"), invalid_ion_remap);
-
-    parameterize_over_ion(cat, "burble", "one");
-    parameterize_over_ion(cat, "burble", "two");
-
-    auto b1 = cat["burble/one"];
-    EXPECT_EQ("one", b1.ions.begin()->first);
-
-    auto b2 = cat["burble/two"];
-    EXPECT_EQ("two", b2.ions.begin()->first);
+    mechanism_info burble_derived_info = cat["burble/quux=3,xyzzy=4"];
+    EXPECT_EQ(3, burble_derived_info.globals["quux"].default_value);
+    EXPECT_EQ(4, burble_derived_info.globals["xyzzy"].default_value);
+
+    // If the mechanism is already in the catalogue though, don't make a new derivation.
+    cat.derive("fleeb/plugh=5", "fleeb", {{"plugh", 7.0}}, {});
+    mechanism_info deceptive = cat["fleeb/plugh=5"];
+    EXPECT_EQ(7, deceptive.globals["plugh"].default_value);
+
+    // Check ion rebinds, too.
+    mechanism_info fleeb_derived_info = cat["fleeb/plugh=2,a=foo,b=bar"];
+    EXPECT_EQ(2, fleeb_derived_info.globals["plugh"].default_value);
+    EXPECT_FALSE(fleeb_derived_info.ions.count("a"));
+    EXPECT_FALSE(fleeb_derived_info.ions.count("b"));
+    EXPECT_TRUE(fleeb_derived_info.ions.count("foo"));
+    EXPECT_TRUE(fleeb_derived_info.ions.count("bar"));
+
+    // If only one ion, don't need to give lhs in reassignment.
+    mechanism_info bleeble_derived_info = cat["bleeble/fish,quux=9"];
+    EXPECT_EQ(9, bleeble_derived_info.globals["quux"].default_value);
+    EXPECT_EQ(-20, bleeble_derived_info.globals["xyzzy"].default_value);
+    EXPECT_EQ(1u, bleeble_derived_info.ions.size());
+    EXPECT_TRUE(bleeble_derived_info.ions.count("fish"));
+
+    // Can't omit lhs if there is more than one ion though.
+    EXPECT_THROW(cat["fleeb/fish"], invalid_ion_remap);
+
+    // Implicitly derived mechanisms should inherit implementations.
+    auto fleeb2 = cat.instance<foo_backend>("fleeb2");
+    auto fleeb2_derived = cat.instance<foo_backend>("fleeb2/plugh=4.5");
+    EXPECT_EQ("special fleeb", fleeb2.mech->internal_name());
+    EXPECT_EQ("special fleeb", fleeb2_derived.mech->internal_name());
+    EXPECT_EQ(4.5, fleeb2_derived.overrides.globals.at("plugh"));
+
+    // Requesting an implicitly derived instance with improper parameters should throw.
+    EXPECT_THROW(cat.instance<foo_backend>("fleeb2/fidget=7"), no_such_parameter);
+
+    // Testing for implicit derivation though should not throw.
+    EXPECT_TRUE(cat.has("fleeb2/plugh=7"));
+    EXPECT_FALSE(cat.has("fleeb2/fidget=7"));
+    EXPECT_TRUE(cat.is_derived("fleeb2/plugh=7"));
+    EXPECT_FALSE(cat.is_derived("fleeb2/fidget=7"));
 }
 
 TEST(mechcat, copy) {