diff --git a/controller/automake.mk b/controller/automake.mk
index 0dbbd5d26b..a17ff0d60b 100644
--- a/controller/automake.mk
+++ b/controller/automake.mk
@@ -14,6 +14,8 @@ controller_ovn_controller_SOURCES = \
 	controller/if-status.h \
 	controller/ip-mcast.c \
 	controller/ip-mcast.h \
+	controller/lb.c \
+	controller/lb.h \
 	controller/lflow.c \
 	controller/lflow.h \
 	controller/lflow-cache.c \
diff --git a/controller/lb.c b/controller/lb.c
new file mode 100644
index 0000000000..8f9f20ed54
--- /dev/null
+++ b/controller/lb.c
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2024, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+/* OpenvSwitch lib includes. */
+#include "openvswitch/vlog.h"
+#include "lib/smap.h"
+
+/* OVN includes */
+#include "lb.h"
+#include "lib/ovn-sb-idl.h"
+#include "ovn/lex.h"
+
+VLOG_DEFINE_THIS_MODULE(controller_lb);
+
+static void
+ovn_lb_get_hairpin_snat_ip(const struct uuid *lb_uuid,
+                           const struct smap *lb_options,
+                           struct lport_addresses *hairpin_addrs)
+{
+    const char *addresses = smap_get(lb_options, "hairpin_snat_ip");
+
+    if (!addresses) {
+        return;
+    }
+
+    if (!extract_ip_address(addresses, hairpin_addrs)) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+        VLOG_WARN_RL(&rl, "bad hairpin_snat_ip %s in load balancer "UUID_FMT,
+                     addresses, UUID_ARGS(lb_uuid));
+    }
+}
+
+struct ovn_controller_lb *
+ovn_controller_lb_create(const struct sbrec_load_balancer *sbrec_lb,
+                         const struct smap *template_vars,
+                         struct sset *template_vars_ref)
+{
+    struct ovn_controller_lb *lb = xzalloc(sizeof *lb);
+    bool template = smap_get_bool(&sbrec_lb->options, "template", false);
+
+    lb->slb = sbrec_lb;
+    lb->n_vips = smap_count(&sbrec_lb->vips);
+    lb->vips = xcalloc(lb->n_vips, sizeof *lb->vips);
+
+    struct smap_node *node;
+    size_t n_vips = 0;
+
+    SMAP_FOR_EACH (node, &sbrec_lb->vips) {
+        struct ovn_lb_vip *lb_vip = &lb->vips[n_vips];
+
+        struct lex_str key_s = template
+                               ? lexer_parse_template_string(node->key,
+                                                             template_vars,
+                                                             template_vars_ref)
+                               : lex_str_use(node->key);
+        struct lex_str value_s = template
+                               ? lexer_parse_template_string(node->value,
+                                                             template_vars,
+                                                             template_vars_ref)
+                               : lex_str_use(node->value);
+        char *error = ovn_lb_vip_init_explicit(lb_vip,
+                                               lex_str_get(&key_s),
+                                               lex_str_get(&value_s));
+        if (error) {
+            free(error);
+        } else {
+            n_vips++;
+        }
+        lex_str_free(&key_s);
+        lex_str_free(&value_s);
+    }
+
+    lb->proto = IPPROTO_TCP;
+    if (sbrec_lb->protocol && sbrec_lb->protocol[0]) {
+        if (!strcmp(sbrec_lb->protocol, "udp")) {
+            lb->proto = IPPROTO_UDP;
+        } else if (!strcmp(sbrec_lb->protocol, "sctp")) {
+            lb->proto = IPPROTO_SCTP;
+        }
+    }
+
+    /* It's possible that parsing VIPs fails.  Update the lb->n_vips to the
+     * correct value.
+     */
+    lb->n_vips = n_vips;
+
+    lb->hairpin_orig_tuple = smap_get_bool(&sbrec_lb->options,
+                                           "hairpin_orig_tuple",
+                                           false);
+    lb->ct_flush = smap_get_bool(&sbrec_lb->options, "ct_flush", false);
+    ovn_lb_get_hairpin_snat_ip(&sbrec_lb->header_.uuid, &sbrec_lb->options,
+                               &lb->hairpin_snat_ips);
+    return lb;
+}
+
+void
+ovn_controller_lb_destroy(struct ovn_controller_lb *lb)
+{
+    for (size_t i = 0; i < lb->n_vips; i++) {
+        ovn_lb_vip_destroy(&lb->vips[i]);
+    }
+    free(lb->vips);
+    destroy_lport_addresses(&lb->hairpin_snat_ips);
+    free(lb);
+}
+
+void
+ovn_controller_lbs_destroy(struct hmap *ovn_controller_lbs)
+{
+    struct ovn_controller_lb *lb;
+    HMAP_FOR_EACH_POP (lb, hmap_node, ovn_controller_lbs) {
+        ovn_controller_lb_destroy(lb);
+    }
+
+    hmap_destroy(ovn_controller_lbs);
+}
+
+struct ovn_controller_lb *
+ovn_controller_lb_find(const struct hmap *ovn_controller_lbs,
+                       const struct uuid *uuid)
+{
+    struct ovn_controller_lb *lb;
+    size_t hash = uuid_hash(uuid);
+    HMAP_FOR_EACH_WITH_HASH (lb, hmap_node, hash, ovn_controller_lbs) {
+        if (uuid_equals(&lb->slb->header_.uuid, uuid)) {
+            return lb;
+        }
+    }
+    return NULL;
+}
+
diff --git a/controller/lb.h b/controller/lb.h
new file mode 100644
index 0000000000..84d51c3329
--- /dev/null
+++ b/controller/lb.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2024, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OVN_CONTROLLER_LB_H
+#define OVN_CONTROLLER_LB_H 1
+
+#include "lib/lb.h"
+
+struct sbrec_load_balancer;
+
+struct ovn_controller_lb {
+    struct hmap_node hmap_node;
+
+    const struct sbrec_load_balancer *slb; /* May be NULL. */
+
+    uint8_t proto;
+
+    struct ovn_lb_vip *vips;
+    size_t n_vips;
+    bool hairpin_orig_tuple; /* True if ovn-northd stores the original
+                              * destination tuple in registers.
+                              */
+    bool ct_flush; /* True if we should flush CT after backend removal. */
+
+    struct lport_addresses hairpin_snat_ips; /* IP (v4 and/or v6) to be used
+                                              * as source for hairpinned
+                                              * traffic.
+                                              */
+};
+
+struct ovn_controller_lb *ovn_controller_lb_create(
+    const struct sbrec_load_balancer *,
+    const struct smap *template_vars,
+    struct sset *template_vars_ref);
+void ovn_controller_lb_destroy(struct ovn_controller_lb *);
+void ovn_controller_lbs_destroy(struct hmap *ovn_controller_lbs);
+struct ovn_controller_lb *ovn_controller_lb_find(
+    const struct hmap *ovn_controller_lbs,
+    const struct uuid *uuid);
+
+#endif /* OVN_CONTROLLER_LB_H */
+
diff --git a/controller/lflow.c b/controller/lflow.c
index c0cf0aa106..895d17d193 100644
--- a/controller/lflow.c
+++ b/controller/lflow.c
@@ -18,6 +18,7 @@
 #include "lflow.h"
 #include "coverage.h"
 #include "ha-chassis.h"
+#include "lb.h"
 #include "lflow-cache.h"
 #include "local_data.h"
 #include "lport.h"
diff --git a/lib/lb.c b/lib/lb.c
index d0d562b6fb..e67a5fcfd0 100644
--- a/lib/lb.c
+++ b/lib/lb.c
@@ -16,76 +16,16 @@
 #include <config.h>
 
 #include "lb.h"
-#include "lib/ovn-nb-idl.h"
-#include "lib/ovn-sb-idl.h"
 #include "lib/ovn-util.h"
-#include "northd/northd.h"
 #include "ovn/lex.h"
 
 /* OpenvSwitch lib includes. */
 #include "openvswitch/vlog.h"
-#include "lib/bitmap.h"
-#include "lib/smap.h"
-#include "socket-util.h"
 
 VLOG_DEFINE_THIS_MODULE(lb);
 
-static const char *lb_neighbor_responder_mode_names[] = {
-    [LB_NEIGH_RESPOND_REACHABLE] = "reachable",
-    [LB_NEIGH_RESPOND_ALL] = "all",
-    [LB_NEIGH_RESPOND_NONE] = "none",
-};
-
-static struct nbrec_load_balancer_health_check *
-ovn_lb_get_health_check(const struct nbrec_load_balancer *nbrec_lb,
-                        const char *vip_port_str, bool template);
 static void ovn_lb_backends_clear(struct ovn_lb_vip *vip);
 
-struct ovn_lb_ip_set *
-ovn_lb_ip_set_create(void)
-{
-    struct ovn_lb_ip_set *lb_ip_set = xzalloc(sizeof *lb_ip_set);
-
-    sset_init(&lb_ip_set->ips_v4);
-    sset_init(&lb_ip_set->ips_v4_routable);
-    sset_init(&lb_ip_set->ips_v4_reachable);
-    sset_init(&lb_ip_set->ips_v6);
-    sset_init(&lb_ip_set->ips_v6_routable);
-    sset_init(&lb_ip_set->ips_v6_reachable);
-
-    return lb_ip_set;
-}
-
-void
-ovn_lb_ip_set_destroy(struct ovn_lb_ip_set *lb_ip_set)
-{
-    if (!lb_ip_set) {
-        return;
-    }
-    sset_destroy(&lb_ip_set->ips_v4);
-    sset_destroy(&lb_ip_set->ips_v4_routable);
-    sset_destroy(&lb_ip_set->ips_v4_reachable);
-    sset_destroy(&lb_ip_set->ips_v6);
-    sset_destroy(&lb_ip_set->ips_v6_routable);
-    sset_destroy(&lb_ip_set->ips_v6_reachable);
-    free(lb_ip_set);
-}
-
-struct ovn_lb_ip_set *
-ovn_lb_ip_set_clone(struct ovn_lb_ip_set *lb_ip_set)
-{
-    struct ovn_lb_ip_set *clone = ovn_lb_ip_set_create();
-
-    sset_clone(&clone->ips_v4, &lb_ip_set->ips_v4);
-    sset_clone(&clone->ips_v4_routable, &lb_ip_set->ips_v4_routable);
-    sset_clone(&clone->ips_v4_reachable, &lb_ip_set->ips_v4_reachable);
-    sset_clone(&clone->ips_v6, &lb_ip_set->ips_v6);
-    sset_clone(&clone->ips_v6_routable, &lb_ip_set->ips_v6_routable);
-    sset_clone(&clone->ips_v6_reachable, &lb_ip_set->ips_v6_reachable);
-
-    return clone;
-}
-
 /* Format for backend ips: "IP1:port1,IP2:port2,...". */
 static char *
 ovn_lb_backends_init_explicit(struct ovn_lb_vip *lb_vip, const char *value)
@@ -160,9 +100,9 @@ ovn_lb_backends_init_explicit(struct ovn_lb_vip *lb_vip, const char *value)
     return NULL;
 }
 
-static
-char *ovn_lb_vip_init_explicit(struct ovn_lb_vip *lb_vip, const char *lb_key,
-                               const char *lb_value)
+char *
+ovn_lb_vip_init_explicit(struct ovn_lb_vip *lb_vip, const char *lb_key,
+                         const char *lb_value)
 {
     if (!ip_address_and_port_from_lb_key(lb_key, &lb_vip->vip_str,
                                          &lb_vip->vip, &lb_vip->vip_port,
@@ -369,22 +309,6 @@ ovn_lb_vip_format__(const struct ovn_lb_vip *vip, struct ds *s,
     }
 }
 
-/* Formats the VIP in the way the ovn-controller expects it, that is,
- * template IPv6 variables need to be between brackets too.
- */
-static char *
-ovn_lb_vip6_template_format_internal(const struct ovn_lb_vip *vip)
-{
-    struct ds s = DS_EMPTY_INITIALIZER;
-
-    if (vip->vip_str && *vip->vip_str == LEX_TEMPLATE_PREFIX) {
-        ovn_lb_vip_format__(vip, &s, true);
-    } else {
-        ovn_lb_vip_format(vip, &s, !!vip->port_str);
-    }
-    return ds_steal_cstr(&s);
-}
-
 void
 ovn_lb_vip_format(const struct ovn_lb_vip *vip, struct ds *s, bool template)
 {
@@ -417,540 +341,20 @@ ovn_lb_vip_backends_format(const struct ovn_lb_vip *vip, struct ds *s)
     }
 }
 
-static
-void ovn_northd_lb_vip_init(struct ovn_northd_lb_vip *lb_vip_nb,
-                            const struct ovn_lb_vip *lb_vip,
-                            const struct nbrec_load_balancer *nbrec_lb,
-                            const char *vip_port_str, const char *backend_ips,
-                            bool template)
-{
-    lb_vip_nb->backend_ips = xstrdup(backend_ips);
-    lb_vip_nb->n_backends = lb_vip->n_backends;
-    lb_vip_nb->backends_nb = xcalloc(lb_vip_nb->n_backends,
-                                     sizeof *lb_vip_nb->backends_nb);
-    lb_vip_nb->lb_health_check =
-        ovn_lb_get_health_check(nbrec_lb, vip_port_str, template);
-}
-
-static void
-ovn_lb_vip_backends_health_check_init(const struct ovn_northd_lb *lb,
-                                      const struct ovn_lb_vip *lb_vip,
-                                      struct ovn_northd_lb_vip *lb_vip_nb)
-{
-    struct ds key = DS_EMPTY_INITIALIZER;
-
-    for (size_t j = 0; j < lb_vip->n_backends; j++) {
-        struct ovn_lb_backend *backend = &lb_vip->backends[j];
-        ds_clear(&key);
-        ds_put_format(&key, IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)
-                      ? "%s" : "[%s]", backend->ip_str);
-
-        const char *s = smap_get(&lb->nlb->ip_port_mappings, ds_cstr(&key));
-        if (!s) {
-            continue;
-        }
-
-        char *svc_mon_src_ip = NULL;
-        char *port_name = xstrdup(s);
-        char *p = strstr(port_name, ":");
-        if (p) {
-            *p = 0;
-            p++;
-            struct sockaddr_storage svc_mon_src_addr;
-            if (!inet_parse_address(p, &svc_mon_src_addr)) {
-                static struct vlog_rate_limit rl =
-                    VLOG_RATE_LIMIT_INIT(5, 1);
-                VLOG_WARN_RL(&rl, "Invalid svc mon src IP %s", p);
-            } else {
-                struct ds src_ip_s = DS_EMPTY_INITIALIZER;
-                ss_format_address_nobracks(&svc_mon_src_addr,
-                                            &src_ip_s);
-                svc_mon_src_ip = ds_steal_cstr(&src_ip_s);
-            }
-        }
-
-        if (svc_mon_src_ip) {
-            struct ovn_northd_lb_backend *backend_nb =
-                &lb_vip_nb->backends_nb[j];
-            backend_nb->health_check = true;
-            backend_nb->logical_port = xstrdup(port_name);
-            backend_nb->svc_mon_src_ip = svc_mon_src_ip;
-        }
-        free(port_name);
-    }
-
-    ds_destroy(&key);
-}
-
-static
-void ovn_northd_lb_vip_destroy(struct ovn_northd_lb_vip *vip)
-{
-    free(vip->backend_ips);
-    for (size_t i = 0; i < vip->n_backends; i++) {
-        free(vip->backends_nb[i].logical_port);
-        free(vip->backends_nb[i].svc_mon_src_ip);
-    }
-    free(vip->backends_nb);
-}
-
-static void
-ovn_lb_get_hairpin_snat_ip(const struct uuid *lb_uuid,
-                           const struct smap *lb_options,
-                           struct lport_addresses *hairpin_addrs)
-{
-    const char *addresses = smap_get(lb_options, "hairpin_snat_ip");
-
-    if (!addresses) {
-        return;
-    }
-
-    if (!extract_ip_address(addresses, hairpin_addrs)) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "bad hairpin_snat_ip %s in load balancer "UUID_FMT,
-                     addresses, UUID_ARGS(lb_uuid));
-    }
-}
-
-static bool
-ovn_lb_get_routable_mode(const struct nbrec_load_balancer *nbrec_lb,
-                         bool routable, bool template)
-{
-    if (template && routable) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-        VLOG_WARN_RL(&rl, "Template load balancer "UUID_FMT" does not suport "
-                           "option 'add_route'.  Forcing it to disabled.",
-                     UUID_ARGS(&nbrec_lb->header_.uuid));
-        return false;
-    }
-    return routable;
-}
-
-static bool
-ovn_lb_neigh_mode_is_valid(enum lb_neighbor_responder_mode mode, bool template)
-{
-    if (!template) {
-        return true;
-    }
-
-    switch (mode) {
-    case LB_NEIGH_RESPOND_REACHABLE:
-        return false;
-    case LB_NEIGH_RESPOND_ALL:
-    case LB_NEIGH_RESPOND_NONE:
-        return true;
-    }
-    return false;
-}
-
-static enum lb_neighbor_responder_mode
-ovn_lb_get_neigh_mode(const struct nbrec_load_balancer *nbrec_lb,
-                      const char *mode, bool template)
-{
-    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-    enum lb_neighbor_responder_mode default_mode =
-        template ? LB_NEIGH_RESPOND_NONE : LB_NEIGH_RESPOND_REACHABLE;
-
-    if (!mode) {
-        mode = lb_neighbor_responder_mode_names[default_mode];
-    }
-
-    for (size_t i = 0; i < ARRAY_SIZE(lb_neighbor_responder_mode_names); i++) {
-        if (!strcmp(mode, lb_neighbor_responder_mode_names[i])) {
-            if (ovn_lb_neigh_mode_is_valid(i, template)) {
-                return i;
-            }
-            break;
-        }
-    }
-
-    VLOG_WARN_RL(&rl, "Invalid neighbor responder mode %s for load balancer "
-                       UUID_FMT", forcing it to %s",
-                 mode, UUID_ARGS(&nbrec_lb->header_.uuid),
-                 lb_neighbor_responder_mode_names[default_mode]);
-    return default_mode;
-}
-
-static struct nbrec_load_balancer_health_check *
-ovn_lb_get_health_check(const struct nbrec_load_balancer *nbrec_lb,
-                        const char *vip_port_str, bool template)
-{
-    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-
-    if (!nbrec_lb->n_health_check) {
-        return NULL;
-    }
-
-    if (nbrec_lb->protocol && !strcmp(nbrec_lb->protocol, "sctp")) {
-        VLOG_WARN_RL(&rl,
-                     "SCTP load balancers do not currently support "
-                     "health checks. Not creating health checks for "
-                     "load balancer " UUID_FMT,
-                     UUID_ARGS(&nbrec_lb->header_.uuid));
-        return NULL;
-    }
-
-    if (template) {
-        VLOG_WARN_RL(&rl,
-                     "Template load balancers do not currently support "
-                     "health checks. Not creating health checks for "
-                     "load balancer " UUID_FMT,
-                     UUID_ARGS(&nbrec_lb->header_.uuid));
-        return NULL;
-    }
-
-    for (size_t i = 0; i < nbrec_lb->n_health_check; i++) {
-        if (!strcmp(nbrec_lb->health_check[i]->vip, vip_port_str)) {
-            return nbrec_lb->health_check[i];
-        }
-    }
-    return NULL;
-}
-
-static void
-ovn_northd_lb_init(struct ovn_northd_lb *lb,
-                   const struct nbrec_load_balancer *nbrec_lb)
-{
-    bool template = smap_get_bool(&nbrec_lb->options, "template", false);
-    bool is_udp = nullable_string_is_equal(nbrec_lb->protocol, "udp");
-    bool is_sctp = nullable_string_is_equal(nbrec_lb->protocol, "sctp");
-    int address_family = !strcmp(smap_get_def(&nbrec_lb->options,
-                                              "address-family", "ipv4"),
-                                 "ipv4")
-                         ? AF_INET
-                         : AF_INET6;
-
-    lb->nlb = nbrec_lb;
-    lb->proto = is_udp ? "udp" : is_sctp ? "sctp" : "tcp";
-    lb->n_vips = smap_count(&nbrec_lb->vips);
-    lb->vips = xcalloc(lb->n_vips, sizeof *lb->vips);
-    lb->vips_nb = xcalloc(lb->n_vips, sizeof *lb->vips_nb);
-    smap_init(&lb->template_vips);
-    lb->controller_event = smap_get_bool(&nbrec_lb->options, "event", false);
-
-    bool routable = smap_get_bool(&nbrec_lb->options, "add_route", false);
-    lb->routable = ovn_lb_get_routable_mode(nbrec_lb, routable, template);
-
-    lb->skip_snat = smap_get_bool(&nbrec_lb->options, "skip_snat", false);
-    lb->template = template;
-
-    const char *mode = smap_get(&nbrec_lb->options, "neighbor_responder");
-    lb->neigh_mode = ovn_lb_get_neigh_mode(nbrec_lb, mode, template);
-
-    uint32_t affinity_timeout =
-        smap_get_uint(&nbrec_lb->options, "affinity_timeout", 0);
-    if (affinity_timeout > UINT16_MAX) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-        VLOG_WARN_RL(&rl, "max affinity_timeout timeout value is %u",
-                     UINT16_MAX);
-        affinity_timeout = UINT16_MAX;
-    }
-    lb->affinity_timeout = affinity_timeout;
-
-    sset_init(&lb->ips_v4);
-    sset_init(&lb->ips_v6);
-    struct smap_node *node;
-    size_t n_vips = 0;
-
-    SMAP_FOR_EACH (node, &nbrec_lb->vips) {
-        struct ovn_lb_vip *lb_vip = &lb->vips[n_vips];
-        struct ovn_northd_lb_vip *lb_vip_nb = &lb->vips_nb[n_vips];
-
-        char *error = ovn_lb_vip_init(lb_vip, node->key, node->value,
-                                      template, address_family);
-        if (error) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-            VLOG_WARN_RL(&rl, "Failed to initialize LB VIP: %s", error);
-            ovn_lb_vip_destroy(lb_vip);
-            free(error);
-            continue;
-        }
-        lb_vip->empty_backend_rej = smap_get_bool(&nbrec_lb->options,
-                                                  "reject", false);
-        ovn_northd_lb_vip_init(lb_vip_nb, lb_vip, nbrec_lb,
-                               node->key, node->value, template);
-        if (lb_vip_nb->lb_health_check) {
-            lb->health_checks = true;
-        }
-
-        if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
-            sset_add(&lb->ips_v4, lb_vip->vip_str);
-        } else {
-            sset_add(&lb->ips_v6, lb_vip->vip_str);
-        }
-
-        if (lb->template && address_family == AF_INET6) {
-            smap_add_nocopy(&lb->template_vips,
-                            ovn_lb_vip6_template_format_internal(lb_vip),
-                            xstrdup(node->value));
-        }
-        n_vips++;
-
-        if (lb_vip_nb->lb_health_check) {
-            ovn_lb_vip_backends_health_check_init(lb, lb_vip, lb_vip_nb);
-        }
-    }
-
-    /* It's possible that parsing VIPs fails.  Update the lb->n_vips to the
-     * correct value.
-     */
-    lb->n_vips = n_vips;
-
-    if (nbrec_lb->n_selection_fields) {
-        char *proto = NULL;
-        if (nbrec_lb->protocol && nbrec_lb->protocol[0]) {
-            proto = nbrec_lb->protocol;
-        }
-
-        struct ds sel_fields = DS_EMPTY_INITIALIZER;
-        for (size_t i = 0; i < lb->nlb->n_selection_fields; i++) {
-            char *field = lb->nlb->selection_fields[i];
-            if (!strcmp(field, "tp_src") && proto) {
-                ds_put_format(&sel_fields, "%s_src,", proto);
-            } else if (!strcmp(field, "tp_dst") && proto) {
-                ds_put_format(&sel_fields, "%s_dst,", proto);
-            } else {
-                ds_put_format(&sel_fields, "%s,", field);
-            }
-        }
-        ds_chomp(&sel_fields, ',');
-        lb->selection_fields = ds_steal_cstr(&sel_fields);
-    }
-}
-
-struct ovn_northd_lb *
-ovn_northd_lb_create(const struct nbrec_load_balancer *nbrec_lb)
-{
-    struct ovn_northd_lb *lb = xzalloc(sizeof *lb);
-    ovn_northd_lb_init(lb, nbrec_lb);
-    return lb;
-}
-
-struct ovn_northd_lb *
-ovn_northd_lb_find(const struct hmap *lbs, const struct uuid *uuid)
-{
-    struct ovn_northd_lb *lb;
-    size_t hash = uuid_hash(uuid);
-    HMAP_FOR_EACH_WITH_HASH (lb, hmap_node, hash, lbs) {
-        if (uuid_equals(&lb->nlb->header_.uuid, uuid)) {
-            return lb;
-        }
-    }
-    return NULL;
-}
-
-const struct smap *
-ovn_northd_lb_get_vips(const struct ovn_northd_lb *lb)
-{
-    if (!smap_is_empty(&lb->template_vips)) {
-        return &lb->template_vips;
-    }
-    return &lb->nlb->vips;
-}
-
-static void
-ovn_northd_lb_cleanup(struct ovn_northd_lb *lb)
-{
-    for (size_t i = 0; i < lb->n_vips; i++) {
-        ovn_lb_vip_destroy(&lb->vips[i]);
-        ovn_northd_lb_vip_destroy(&lb->vips_nb[i]);
-    }
-    free(lb->vips);
-    free(lb->vips_nb);
-    lb->vips = NULL;
-    lb->vips_nb = NULL;
-    smap_destroy(&lb->template_vips);
-    sset_destroy(&lb->ips_v4);
-    sset_destroy(&lb->ips_v6);
-    free(lb->selection_fields);
-    lb->selection_fields = NULL;
-    lb->health_checks = false;
-}
-
-void
-ovn_northd_lb_destroy(struct ovn_northd_lb *lb)
-{
-    ovn_northd_lb_cleanup(lb);
-    free(lb);
-}
-
-void
-ovn_northd_lb_reinit(struct ovn_northd_lb *lb,
-                     const struct nbrec_load_balancer *nbrec_lb)
-{
-    ovn_northd_lb_cleanup(lb);
-    ovn_northd_lb_init(lb, nbrec_lb);
-}
-
-static void
-ovn_lb_group_init(struct ovn_lb_group *lb_group,
-                  const struct nbrec_load_balancer_group *nbrec_lb_group,
-                  const struct hmap *lbs)
-{
-    lb_group->n_lbs = nbrec_lb_group->n_load_balancer;
-    lb_group->lbs = xmalloc(lb_group->n_lbs * sizeof *lb_group->lbs);
-    lb_group->lb_ips = ovn_lb_ip_set_create();
-
-    for (size_t i = 0; i < nbrec_lb_group->n_load_balancer; i++) {
-        const struct uuid *lb_uuid =
-            &nbrec_lb_group->load_balancer[i]->header_.uuid;
-        lb_group->lbs[i] = ovn_northd_lb_find(lbs, lb_uuid);
-        lb_group->has_routable_lb |= lb_group->lbs[i]->routable;
-    }
-}
-
-/* Constructs a new 'struct ovn_lb_group' object from the Nb LB Group record
- * and an array of 'struct ovn_northd_lb' objects for its associated
- * load balancers. */
-struct ovn_lb_group *
-ovn_lb_group_create(const struct nbrec_load_balancer_group *nbrec_lb_group,
-                    const struct hmap *lbs)
-{
-    struct ovn_lb_group *lb_group = xzalloc(sizeof *lb_group);
-    lb_group->uuid = nbrec_lb_group->header_.uuid;
-    ovn_lb_group_init(lb_group, nbrec_lb_group, lbs);
-    return lb_group;
-}
-
-static void
-ovn_lb_group_cleanup(struct ovn_lb_group *lb_group)
-{
-    ovn_lb_ip_set_destroy(lb_group->lb_ips);
-    lb_group->lb_ips = NULL;
-    lb_group->has_routable_lb = false;
-    free(lb_group->lbs);
-}
-
-void
-ovn_lb_group_destroy(struct ovn_lb_group *lb_group)
-{
-    if (!lb_group) {
-        return;
-    }
-
-    ovn_lb_group_cleanup(lb_group);
-    free(lb_group);
-}
-
-void
-ovn_lb_group_reinit(struct ovn_lb_group *lb_group,
-                    const struct nbrec_load_balancer_group *nbrec_lb_group,
-                    const struct hmap *lbs)
-{
-    ovn_lb_group_cleanup(lb_group);
-    ovn_lb_group_init(lb_group, nbrec_lb_group, lbs);
-}
-
-struct ovn_lb_group *
-ovn_lb_group_find(const struct hmap *lb_groups, const struct uuid *uuid)
-{
-    struct ovn_lb_group *lb_group;
-    size_t hash = uuid_hash(uuid);
-
-    HMAP_FOR_EACH_WITH_HASH (lb_group, hmap_node, hash, lb_groups) {
-        if (uuid_equals(&lb_group->uuid, uuid)) {
-            return lb_group;
-        }
-    }
-    return NULL;
-}
-
-struct ovn_controller_lb *
-ovn_controller_lb_create(const struct sbrec_load_balancer *sbrec_lb,
-                         const struct smap *template_vars,
-                         struct sset *template_vars_ref)
-{
-    struct ovn_controller_lb *lb = xzalloc(sizeof *lb);
-    bool template = smap_get_bool(&sbrec_lb->options, "template", false);
-
-    lb->slb = sbrec_lb;
-    lb->n_vips = smap_count(&sbrec_lb->vips);
-    lb->vips = xcalloc(lb->n_vips, sizeof *lb->vips);
-
-    struct smap_node *node;
-    size_t n_vips = 0;
-
-    SMAP_FOR_EACH (node, &sbrec_lb->vips) {
-        struct ovn_lb_vip *lb_vip = &lb->vips[n_vips];
-
-        struct lex_str key_s = template
-                               ? lexer_parse_template_string(node->key,
-                                                             template_vars,
-                                                             template_vars_ref)
-                               : lex_str_use(node->key);
-        struct lex_str value_s = template
-                               ? lexer_parse_template_string(node->value,
-                                                             template_vars,
-                                                             template_vars_ref)
-                               : lex_str_use(node->value);
-        char *error = ovn_lb_vip_init_explicit(lb_vip,
-                                               lex_str_get(&key_s),
-                                               lex_str_get(&value_s));
-        if (error) {
-            free(error);
-        } else {
-            n_vips++;
-        }
-        lex_str_free(&key_s);
-        lex_str_free(&value_s);
-    }
-
-    lb->proto = IPPROTO_TCP;
-    if (sbrec_lb->protocol && sbrec_lb->protocol[0]) {
-        if (!strcmp(sbrec_lb->protocol, "udp")) {
-            lb->proto = IPPROTO_UDP;
-        } else if (!strcmp(sbrec_lb->protocol, "sctp")) {
-            lb->proto = IPPROTO_SCTP;
-        }
-    }
-
-    /* It's possible that parsing VIPs fails.  Update the lb->n_vips to the
-     * correct value.
-     */
-    lb->n_vips = n_vips;
-
-    lb->hairpin_orig_tuple = smap_get_bool(&sbrec_lb->options,
-                                           "hairpin_orig_tuple",
-                                           false);
-    lb->ct_flush = smap_get_bool(&sbrec_lb->options, "ct_flush", false);
-    ovn_lb_get_hairpin_snat_ip(&sbrec_lb->header_.uuid, &sbrec_lb->options,
-                               &lb->hairpin_snat_ips);
-    return lb;
-}
-
-void
-ovn_controller_lb_destroy(struct ovn_controller_lb *lb)
-{
-    for (size_t i = 0; i < lb->n_vips; i++) {
-        ovn_lb_vip_destroy(&lb->vips[i]);
-    }
-    free(lb->vips);
-    destroy_lport_addresses(&lb->hairpin_snat_ips);
-    free(lb);
-}
-
-void
-ovn_controller_lbs_destroy(struct hmap *ovn_controller_lbs)
+/* Formats the VIP in the way the ovn-controller expects it, that is,
+ * template IPv6 variables need to be between brackets too.
+ */
+char *
+ovn_lb_vip6_template_format_internal(const struct ovn_lb_vip *vip)
 {
-    struct ovn_controller_lb *lb;
-    HMAP_FOR_EACH_POP (lb, hmap_node, ovn_controller_lbs) {
-        ovn_controller_lb_destroy(lb);
-    }
-
-    hmap_destroy(ovn_controller_lbs);
-}
+    struct ds s = DS_EMPTY_INITIALIZER;
 
-struct ovn_controller_lb *
-ovn_controller_lb_find(const struct hmap *ovn_controller_lbs,
-                       const struct uuid *uuid)
-{
-    struct ovn_controller_lb *lb;
-    size_t hash = uuid_hash(uuid);
-    HMAP_FOR_EACH_WITH_HASH (lb, hmap_node, hash, ovn_controller_lbs) {
-        if (uuid_equals(&lb->slb->header_.uuid, uuid)) {
-            return lb;
-        }
+    if (vip->vip_str && *vip->vip_str == LEX_TEMPLATE_PREFIX) {
+        ovn_lb_vip_format__(vip, &s, true);
+    } else {
+        ovn_lb_vip_format(vip, &s, !!vip->port_str);
     }
-    return NULL;
+    return ds_steal_cstr(&s);
 }
 
 static uint32_t
@@ -1020,150 +424,3 @@ ovn_lb_5tuples_destroy(struct hmap *tuples)
 
     hmap_destroy(tuples);
 }
-
-void
-build_lrouter_lb_ips(struct ovn_lb_ip_set *lb_ips,
-                     const struct ovn_northd_lb *lb)
-{
-    add_ips_to_lb_ip_set(lb_ips, lb->routable, &lb->ips_v4, &lb->ips_v6);
-}
-
-void
-add_ips_to_lb_ip_set(struct ovn_lb_ip_set *lb_ips,
-                     bool is_routable,
-                     const struct sset *lb_ips_v4,
-                     const struct sset *lb_ips_v6)
-{
-    const char *ip_address;
-
-    SSET_FOR_EACH (ip_address, lb_ips_v4) {
-        sset_add(&lb_ips->ips_v4, ip_address);
-        if (is_routable) {
-            sset_add(&lb_ips->ips_v4_routable, ip_address);
-        }
-    }
-    SSET_FOR_EACH (ip_address, lb_ips_v6) {
-        sset_add(&lb_ips->ips_v6, ip_address);
-        if (is_routable) {
-            sset_add(&lb_ips->ips_v6_routable, ip_address);
-        }
-    }
-}
-
-void
-remove_ips_from_lb_ip_set(struct ovn_lb_ip_set *lb_ips,
-                          bool is_routable,
-                          const struct sset *lb_ips_v4,
-                          const struct sset *lb_ips_v6)
-{
-    const char *ip_address;
-
-    SSET_FOR_EACH (ip_address, lb_ips_v4) {
-        sset_find_and_delete(&lb_ips->ips_v4, ip_address);
-        if (is_routable) {
-            sset_find_and_delete(&lb_ips->ips_v4_routable, ip_address);
-        }
-    }
-    SSET_FOR_EACH (ip_address, lb_ips_v6) {
-        sset_find_and_delete(&lb_ips->ips_v6, ip_address);
-        if (is_routable) {
-            sset_find_and_delete(&lb_ips->ips_v6_routable, ip_address);
-        }
-    }
-}
-
-/* lb datapaths functions */
-struct  ovn_lb_datapaths *
-ovn_lb_datapaths_create(const struct ovn_northd_lb *lb, size_t n_ls_datapaths,
-                        size_t n_lr_datapaths)
-{
-    struct ovn_lb_datapaths *lb_dps = xzalloc(sizeof *lb_dps);
-    lb_dps->lb = lb;
-    lb_dps->nb_ls_map = bitmap_allocate(n_ls_datapaths);
-    lb_dps->nb_lr_map = bitmap_allocate(n_lr_datapaths);
-
-    return lb_dps;
-}
-
-struct ovn_lb_datapaths *
-ovn_lb_datapaths_find(const struct hmap *lb_dps_map,
-                      const struct uuid *lb_uuid)
-{
-    struct ovn_lb_datapaths *lb_dps;
-    size_t hash = uuid_hash(lb_uuid);
-    HMAP_FOR_EACH_WITH_HASH (lb_dps, hmap_node, hash, lb_dps_map) {
-        if (uuid_equals(&lb_dps->lb->nlb->header_.uuid, lb_uuid)) {
-            return lb_dps;
-        }
-    }
-    return NULL;
-}
-
-void
-ovn_lb_datapaths_destroy(struct ovn_lb_datapaths *lb_dps)
-{
-    bitmap_free(lb_dps->nb_lr_map);
-    bitmap_free(lb_dps->nb_ls_map);
-    free(lb_dps);
-}
-
-void
-ovn_lb_datapaths_add_lr(struct ovn_lb_datapaths *lb_dps, size_t n,
-                        struct ovn_datapath **ods)
-{
-    for (size_t i = 0; i < n; i++) {
-        if (!bitmap_is_set(lb_dps->nb_lr_map, ods[i]->index)) {
-            bitmap_set1(lb_dps->nb_lr_map, ods[i]->index);
-            lb_dps->n_nb_lr++;
-        }
-    }
-}
-
-void
-ovn_lb_datapaths_add_ls(struct ovn_lb_datapaths *lb_dps, size_t n,
-                        struct ovn_datapath **ods)
-{
-    for (size_t i = 0; i < n; i++) {
-        if (!bitmap_is_set(lb_dps->nb_ls_map, ods[i]->index)) {
-            bitmap_set1(lb_dps->nb_ls_map, ods[i]->index);
-            lb_dps->n_nb_ls++;
-        }
-    }
-}
-
-struct ovn_lb_group_datapaths *
-ovn_lb_group_datapaths_create(const struct ovn_lb_group *lb_group,
-                              size_t max_ls_datapaths,
-                              size_t max_lr_datapaths)
-{
-    struct ovn_lb_group_datapaths *lb_group_dps =
-        xzalloc(sizeof *lb_group_dps);
-    lb_group_dps->lb_group = lb_group;
-    lb_group_dps->ls = xmalloc(max_ls_datapaths * sizeof *lb_group_dps->ls);
-    lb_group_dps->lr = xmalloc(max_lr_datapaths * sizeof *lb_group_dps->lr);
-
-    return lb_group_dps;
-}
-
-void
-ovn_lb_group_datapaths_destroy(struct ovn_lb_group_datapaths *lb_group_dps)
-{
-    free(lb_group_dps->ls);
-    free(lb_group_dps->lr);
-    free(lb_group_dps);
-}
-
-struct ovn_lb_group_datapaths *
-ovn_lb_group_datapaths_find(const struct hmap *lb_group_dps_map,
-                            const struct uuid *lb_group_uuid)
-{
-    struct ovn_lb_group_datapaths *lb_group_dps;
-    size_t hash = uuid_hash(lb_group_uuid);
-
-    HMAP_FOR_EACH_WITH_HASH (lb_group_dps, hmap_node, hash, lb_group_dps_map) {
-        if (uuid_equals(&lb_group_dps->lb_group->uuid, lb_group_uuid)) {
-            return lb_group_dps;
-        }
-    }
-    return NULL;
-}
diff --git a/lib/lb.h b/lib/lb.h
index b8e3c1e8fb..bcc677f82e 100644
--- a/lib/lb.h
+++ b/lib/lb.h
@@ -25,63 +25,8 @@
 #include "sset.h"
 #include "uuid.h"
 
-struct nbrec_load_balancer;
-struct nbrec_load_balancer_group;
-struct sbrec_load_balancer;
-struct sbrec_datapath_binding;
-struct ovn_datapath;
-struct ovn_dp_group;
-struct ovn_port;
 struct uuid;
 
-enum lb_neighbor_responder_mode {
-    LB_NEIGH_RESPOND_REACHABLE,
-    LB_NEIGH_RESPOND_ALL,
-    LB_NEIGH_RESPOND_NONE,
-};
-
-/* The "routable" ssets are subsets of the load balancer IPs for which IP
- * routes and ARP resolution flows are automatically added. */
-struct ovn_lb_ip_set {
-    struct sset ips_v4;
-    struct sset ips_v4_routable;
-    struct sset ips_v4_reachable;
-    struct sset ips_v6;
-    struct sset ips_v6_routable;
-    struct sset ips_v6_reachable;
-};
-
-struct ovn_lb_ip_set *ovn_lb_ip_set_create(void);
-void ovn_lb_ip_set_destroy(struct ovn_lb_ip_set *);
-struct ovn_lb_ip_set *ovn_lb_ip_set_clone(struct ovn_lb_ip_set *);
-
-struct ovn_northd_lb {
-    struct hmap_node hmap_node;
-
-    const struct nbrec_load_balancer *nlb; /* May be NULL. */
-    const char *proto;
-    char *selection_fields;
-    struct ovn_lb_vip *vips;
-    struct ovn_northd_lb_vip *vips_nb;
-    struct smap template_vips; /* Slightly changed template VIPs, populated
-                                * if needed.  Until now it's only required
-                                * for IPv6 template load balancers. */
-    size_t n_vips;
-
-    enum lb_neighbor_responder_mode neigh_mode;
-    bool controller_event;
-    bool routable;
-    bool skip_snat;
-    bool template;
-    uint16_t affinity_timeout;
-
-    struct sset ips_v4;
-    struct sset ips_v6;
-
-    /* Indicates if the load balancer has health checks configured. */
-    bool health_checks;
-};
-
 struct ovn_lb_vip {
     struct in6_addr vip; /* Only used in ovn-controller. */
     char *vip_str;       /* Actual VIP string representation (without port).
@@ -113,153 +58,15 @@ struct ovn_lb_backend {
                           */
 };
 
-/* ovn-northd specific backend information. */
-struct ovn_northd_lb_vip {
-    char *backend_ips;
-    struct ovn_northd_lb_backend *backends_nb;
-    size_t n_backends;
-
-    struct nbrec_load_balancer_health_check *lb_health_check;
-};
-
-struct ovn_northd_lb_backend {
-    bool health_check;
-    char *logical_port; /* Logical port to which the ip belong to. */
-    char *svc_mon_src_ip; /* Source IP to use for monitoring. */
-};
-
-struct ovn_northd_lb *ovn_northd_lb_create(const struct nbrec_load_balancer *);
-struct ovn_northd_lb *ovn_northd_lb_find(const struct hmap *,
-                                         const struct uuid *);
-const struct smap *ovn_northd_lb_get_vips(const struct ovn_northd_lb *);
-void ovn_northd_lb_destroy(struct ovn_northd_lb *);
-void ovn_northd_lb_reinit(struct ovn_northd_lb *,
-                          const struct nbrec_load_balancer *);
-
-void build_lrouter_lb_ips(struct ovn_lb_ip_set *,
-                          const struct ovn_northd_lb *);
-void add_ips_to_lb_ip_set(struct ovn_lb_ip_set *lb_ips,
-                          bool is_routable,
-                          const struct sset *lb_ips_v4,
-                          const struct sset *lb_ips_v6);
-void remove_ips_from_lb_ip_set(struct ovn_lb_ip_set *lb_ips,
-                               bool is_routable,
-                               const struct sset *lb_ips_v4,
-                               const struct sset *lb_ips_v6);
-
-struct ovn_lb_group {
-    struct hmap_node hmap_node;
-    struct uuid uuid;
-    size_t n_lbs;
-    struct ovn_northd_lb **lbs;
-    struct ovn_lb_ip_set *lb_ips;
-    bool has_routable_lb;
-};
-
-struct ovn_lb_group *ovn_lb_group_create(
-    const struct nbrec_load_balancer_group *,
-    const struct hmap *lbs);
-void ovn_lb_group_destroy(struct ovn_lb_group *lb_group);
-struct ovn_lb_group *ovn_lb_group_find(const struct hmap *lb_groups,
-                                       const struct uuid *);
-void ovn_lb_group_reinit(
-    struct ovn_lb_group *lb_group,
-    const struct nbrec_load_balancer_group *,
-    const struct hmap *lbs);
-
-struct ovn_lb_datapaths {
-    struct hmap_node hmap_node;
-
-    const struct ovn_northd_lb *lb;
-    size_t n_nb_ls;
-    unsigned long *nb_ls_map;
-
-    size_t n_nb_lr;
-    unsigned long *nb_lr_map;
-};
-
-struct ovn_lb_datapaths *ovn_lb_datapaths_create(const struct ovn_northd_lb *,
-                                                 size_t n_ls_datapaths,
-                                                 size_t n_lr_datapaths);
-struct ovn_lb_datapaths *ovn_lb_datapaths_find(const struct hmap *,
-                                               const struct uuid *);
-void ovn_lb_datapaths_destroy(struct ovn_lb_datapaths *);
-void ovn_lb_datapaths_add_lr(struct ovn_lb_datapaths *, size_t n,
-                             struct ovn_datapath **);
-void ovn_lb_datapaths_add_ls(struct ovn_lb_datapaths *, size_t n,
-                             struct ovn_datapath **);
-
-struct ovn_lb_group_datapaths {
-    struct hmap_node hmap_node;
-
-    const struct ovn_lb_group *lb_group;
-
-    /* Datapaths to which 'lb_group' is applied. */
-    size_t n_ls;
-    struct ovn_datapath **ls;
-    size_t n_lr;
-    struct ovn_datapath **lr;
-};
-
-struct ovn_lb_group_datapaths *ovn_lb_group_datapaths_create(
-    const struct ovn_lb_group *, size_t max_ls_datapaths,
-    size_t max_lr_datapaths);
-
-void ovn_lb_group_datapaths_destroy(struct ovn_lb_group_datapaths *);
-struct ovn_lb_group_datapaths *ovn_lb_group_datapaths_find(
-    const struct hmap *lb_group_dps, const struct uuid *);
-
-static inline void
-ovn_lb_group_datapaths_add_ls(struct ovn_lb_group_datapaths *lbg_dps, size_t n,
-                               struct ovn_datapath **ods)
-{
-    memcpy(&lbg_dps->ls[lbg_dps->n_ls], ods, n * sizeof *ods);
-    lbg_dps->n_ls += n;
-}
-
-static inline void
-ovn_lb_group_datapaths_add_lr(struct ovn_lb_group_datapaths *lbg_dps,
-                               struct ovn_datapath *lr)
-{
-    lbg_dps->lr[lbg_dps->n_lr++] = lr;
-}
-
-struct ovn_controller_lb {
-    struct hmap_node hmap_node;
-
-    const struct sbrec_load_balancer *slb; /* May be NULL. */
-
-    uint8_t proto;
-
-    struct ovn_lb_vip *vips;
-    size_t n_vips;
-    bool hairpin_orig_tuple; /* True if ovn-northd stores the original
-                              * destination tuple in registers.
-                              */
-    bool ct_flush; /* True if we should flush CT after backend removal. */
-
-    struct lport_addresses hairpin_snat_ips; /* IP (v4 and/or v6) to be used
-                                              * as source for hairpinned
-                                              * traffic.
-                                              */
-};
-
-struct ovn_controller_lb *ovn_controller_lb_create(
-    const struct sbrec_load_balancer *,
-    const struct smap *template_vars,
-    struct sset *template_vars_ref);
-void ovn_controller_lb_destroy(struct ovn_controller_lb *);
-void ovn_controller_lbs_destroy(struct hmap *ovn_controller_lbs);
-struct ovn_controller_lb *ovn_controller_lb_find(
-    const struct hmap *ovn_controller_lbs,
-    const struct uuid *uuid);
-
 char *ovn_lb_vip_init(struct ovn_lb_vip *lb_vip, const char *lb_key,
                       const char *lb_value, bool template, int address_family);
+char *ovn_lb_vip_init_explicit(struct ovn_lb_vip *lb_vip, const char *lb_key,
+                               const char *lb_value);
 void ovn_lb_vip_destroy(struct ovn_lb_vip *vip);
 void ovn_lb_vip_format(const struct ovn_lb_vip *vip, struct ds *s,
                        bool template);
 void ovn_lb_vip_backends_format(const struct ovn_lb_vip *vip, struct ds *s);
+char *ovn_lb_vip6_template_format_internal(const struct ovn_lb_vip *vip);
 
 struct ovn_lb_5tuple {
     struct hmap_node hmap_node;
diff --git a/northd/automake.mk b/northd/automake.mk
index 7c6d56a4ff..19abb0dece 100644
--- a/northd/automake.mk
+++ b/northd/automake.mk
@@ -35,7 +35,9 @@ northd_ovn_northd_SOURCES = \
 	northd/ipam.c \
 	northd/ipam.h \
 	northd/lflow-mgr.c \
-	northd/lflow-mgr.h
+	northd/lflow-mgr.h \
+	northd/lb.c \
+	northd/lb.h
 northd_ovn_northd_LDADD = \
 	lib/libovn.la \
 	$(OVSDB_LIBDIR)/libovsdb.la \
diff --git a/northd/en-lb-data.c b/northd/en-lb-data.c
index d06f46a54b..6ad3fbb35f 100644
--- a/northd/en-lb-data.c
+++ b/northd/en-lb-data.c
@@ -25,6 +25,7 @@
 
 /* OVN includes */
 #include "en-lb-data.h"
+#include "lb.h"
 #include "lib/inc-proc-eng.h"
 #include "lib/lb.h"
 #include "lib/ovn-nb-idl.h"
diff --git a/northd/en-lr-stateful.c b/northd/en-lr-stateful.c
index 8665b3c791..3e2a6c254e 100644
--- a/northd/en-lr-stateful.c
+++ b/northd/en-lr-stateful.c
@@ -33,6 +33,7 @@
 #include "en-lb-data.h"
 #include "en-lr-nat.h"
 #include "en-lr-stateful.h"
+#include "lb.h"
 #include "lib/inc-proc-eng.h"
 #include "lib/lb.h"
 #include "lib/ovn-nb-idl.h"
diff --git a/northd/en-sync-sb.c b/northd/en-sync-sb.c
index 53f687f220..9ca59d4338 100644
--- a/northd/en-sync-sb.c
+++ b/northd/en-sync-sb.c
@@ -24,6 +24,7 @@
 #include "en-lr-nat.h"
 #include "en-lr-stateful.h"
 #include "en-sync-sb.h"
+#include "lb.h"
 #include "lib/inc-proc-eng.h"
 #include "lib/lb.h"
 #include "lib/ovn-nb-idl.h"
diff --git a/northd/lb.c b/northd/lb.c
new file mode 100644
index 0000000000..e35569cb70
--- /dev/null
+++ b/northd/lb.c
@@ -0,0 +1,651 @@
+/*
+ * Copyright (c) 2024, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <config.h>
+
+/* OVS includes */
+#include "lib/bitmap.h"
+#include "openvswitch/vlog.h"
+#include "socket-util.h"
+
+/* OVN includes */
+#include "lb.h"
+#include "lib/ovn-nb-idl.h"
+#include "northd.h"
+#include "ovn/lex.h"
+
+VLOG_DEFINE_THIS_MODULE(northd_lb);
+
+static const char *lb_neighbor_responder_mode_names[] = {
+    [LB_NEIGH_RESPOND_REACHABLE] = "reachable",
+    [LB_NEIGH_RESPOND_ALL] = "all",
+    [LB_NEIGH_RESPOND_NONE] = "none",
+};
+
+static struct nbrec_load_balancer_health_check *
+ovn_lb_get_health_check(const struct nbrec_load_balancer *nbrec_lb,
+                        const char *vip_port_str, bool template);
+
+struct ovn_lb_ip_set *
+ovn_lb_ip_set_create(void)
+{
+    struct ovn_lb_ip_set *lb_ip_set = xzalloc(sizeof *lb_ip_set);
+
+    sset_init(&lb_ip_set->ips_v4);
+    sset_init(&lb_ip_set->ips_v4_routable);
+    sset_init(&lb_ip_set->ips_v4_reachable);
+    sset_init(&lb_ip_set->ips_v6);
+    sset_init(&lb_ip_set->ips_v6_routable);
+    sset_init(&lb_ip_set->ips_v6_reachable);
+
+    return lb_ip_set;
+}
+
+void
+ovn_lb_ip_set_destroy(struct ovn_lb_ip_set *lb_ip_set)
+{
+    if (!lb_ip_set) {
+        return;
+    }
+    sset_destroy(&lb_ip_set->ips_v4);
+    sset_destroy(&lb_ip_set->ips_v4_routable);
+    sset_destroy(&lb_ip_set->ips_v4_reachable);
+    sset_destroy(&lb_ip_set->ips_v6);
+    sset_destroy(&lb_ip_set->ips_v6_routable);
+    sset_destroy(&lb_ip_set->ips_v6_reachable);
+    free(lb_ip_set);
+}
+
+struct ovn_lb_ip_set *
+ovn_lb_ip_set_clone(struct ovn_lb_ip_set *lb_ip_set)
+{
+    struct ovn_lb_ip_set *clone = ovn_lb_ip_set_create();
+
+    sset_clone(&clone->ips_v4, &lb_ip_set->ips_v4);
+    sset_clone(&clone->ips_v4_routable, &lb_ip_set->ips_v4_routable);
+    sset_clone(&clone->ips_v4_reachable, &lb_ip_set->ips_v4_reachable);
+    sset_clone(&clone->ips_v6, &lb_ip_set->ips_v6);
+    sset_clone(&clone->ips_v6_routable, &lb_ip_set->ips_v6_routable);
+    sset_clone(&clone->ips_v6_reachable, &lb_ip_set->ips_v6_reachable);
+
+    return clone;
+}
+
+static
+void ovn_northd_lb_vip_init(struct ovn_northd_lb_vip *lb_vip_nb,
+                            const struct ovn_lb_vip *lb_vip,
+                            const struct nbrec_load_balancer *nbrec_lb,
+                            const char *vip_port_str, const char *backend_ips,
+                            bool template)
+{
+    lb_vip_nb->backend_ips = xstrdup(backend_ips);
+    lb_vip_nb->n_backends = lb_vip->n_backends;
+    lb_vip_nb->backends_nb = xcalloc(lb_vip_nb->n_backends,
+                                     sizeof *lb_vip_nb->backends_nb);
+    lb_vip_nb->lb_health_check =
+        ovn_lb_get_health_check(nbrec_lb, vip_port_str, template);
+}
+
+static void
+ovn_lb_vip_backends_health_check_init(const struct ovn_northd_lb *lb,
+                                      const struct ovn_lb_vip *lb_vip,
+                                      struct ovn_northd_lb_vip *lb_vip_nb)
+{
+    struct ds key = DS_EMPTY_INITIALIZER;
+
+    for (size_t j = 0; j < lb_vip->n_backends; j++) {
+        struct ovn_lb_backend *backend = &lb_vip->backends[j];
+        ds_clear(&key);
+        ds_put_format(&key, IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)
+                      ? "%s" : "[%s]", backend->ip_str);
+
+        const char *s = smap_get(&lb->nlb->ip_port_mappings, ds_cstr(&key));
+        if (!s) {
+            continue;
+        }
+
+        char *svc_mon_src_ip = NULL;
+        char *port_name = xstrdup(s);
+        char *p = strstr(port_name, ":");
+        if (p) {
+            *p = 0;
+            p++;
+            struct sockaddr_storage svc_mon_src_addr;
+            if (!inet_parse_address(p, &svc_mon_src_addr)) {
+                static struct vlog_rate_limit rl =
+                    VLOG_RATE_LIMIT_INIT(5, 1);
+                VLOG_WARN_RL(&rl, "Invalid svc mon src IP %s", p);
+            } else {
+                struct ds src_ip_s = DS_EMPTY_INITIALIZER;
+                ss_format_address_nobracks(&svc_mon_src_addr,
+                                            &src_ip_s);
+                svc_mon_src_ip = ds_steal_cstr(&src_ip_s);
+            }
+        }
+
+        if (svc_mon_src_ip) {
+            struct ovn_northd_lb_backend *backend_nb =
+                &lb_vip_nb->backends_nb[j];
+            backend_nb->health_check = true;
+            backend_nb->logical_port = xstrdup(port_name);
+            backend_nb->svc_mon_src_ip = svc_mon_src_ip;
+        }
+        free(port_name);
+    }
+
+    ds_destroy(&key);
+}
+
+static
+void ovn_northd_lb_vip_destroy(struct ovn_northd_lb_vip *vip)
+{
+    free(vip->backend_ips);
+    for (size_t i = 0; i < vip->n_backends; i++) {
+        free(vip->backends_nb[i].logical_port);
+        free(vip->backends_nb[i].svc_mon_src_ip);
+    }
+    free(vip->backends_nb);
+}
+
+static bool
+ovn_lb_get_routable_mode(const struct nbrec_load_balancer *nbrec_lb,
+                         bool routable, bool template)
+{
+    if (template && routable) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+        VLOG_WARN_RL(&rl, "Template load balancer "UUID_FMT" does not suport "
+                           "option 'add_route'.  Forcing it to disabled.",
+                     UUID_ARGS(&nbrec_lb->header_.uuid));
+        return false;
+    }
+    return routable;
+}
+
+static bool
+ovn_lb_neigh_mode_is_valid(enum lb_neighbor_responder_mode mode, bool template)
+{
+    if (!template) {
+        return true;
+    }
+
+    switch (mode) {
+    case LB_NEIGH_RESPOND_REACHABLE:
+        return false;
+    case LB_NEIGH_RESPOND_ALL:
+    case LB_NEIGH_RESPOND_NONE:
+        return true;
+    }
+    return false;
+}
+
+static enum lb_neighbor_responder_mode
+ovn_lb_get_neigh_mode(const struct nbrec_load_balancer *nbrec_lb,
+                      const char *mode, bool template)
+{
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+    enum lb_neighbor_responder_mode default_mode =
+        template ? LB_NEIGH_RESPOND_NONE : LB_NEIGH_RESPOND_REACHABLE;
+
+    if (!mode) {
+        mode = lb_neighbor_responder_mode_names[default_mode];
+    }
+
+    for (size_t i = 0; i < ARRAY_SIZE(lb_neighbor_responder_mode_names); i++) {
+        if (!strcmp(mode, lb_neighbor_responder_mode_names[i])) {
+            if (ovn_lb_neigh_mode_is_valid(i, template)) {
+                return i;
+            }
+            break;
+        }
+    }
+
+    VLOG_WARN_RL(&rl, "Invalid neighbor responder mode %s for load balancer "
+                       UUID_FMT", forcing it to %s",
+                 mode, UUID_ARGS(&nbrec_lb->header_.uuid),
+                 lb_neighbor_responder_mode_names[default_mode]);
+    return default_mode;
+}
+
+static struct nbrec_load_balancer_health_check *
+ovn_lb_get_health_check(const struct nbrec_load_balancer *nbrec_lb,
+                        const char *vip_port_str, bool template)
+{
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+
+    if (!nbrec_lb->n_health_check) {
+        return NULL;
+    }
+
+    if (nbrec_lb->protocol && !strcmp(nbrec_lb->protocol, "sctp")) {
+        VLOG_WARN_RL(&rl,
+                     "SCTP load balancers do not currently support "
+                     "health checks. Not creating health checks for "
+                     "load balancer " UUID_FMT,
+                     UUID_ARGS(&nbrec_lb->header_.uuid));
+        return NULL;
+    }
+
+    if (template) {
+        VLOG_WARN_RL(&rl,
+                     "Template load balancers do not currently support "
+                     "health checks. Not creating health checks for "
+                     "load balancer " UUID_FMT,
+                     UUID_ARGS(&nbrec_lb->header_.uuid));
+        return NULL;
+    }
+
+    for (size_t i = 0; i < nbrec_lb->n_health_check; i++) {
+        if (!strcmp(nbrec_lb->health_check[i]->vip, vip_port_str)) {
+            return nbrec_lb->health_check[i];
+        }
+    }
+    return NULL;
+}
+
+static void
+ovn_northd_lb_init(struct ovn_northd_lb *lb,
+                   const struct nbrec_load_balancer *nbrec_lb)
+{
+    bool template = smap_get_bool(&nbrec_lb->options, "template", false);
+    bool is_udp = nullable_string_is_equal(nbrec_lb->protocol, "udp");
+    bool is_sctp = nullable_string_is_equal(nbrec_lb->protocol, "sctp");
+    int address_family = !strcmp(smap_get_def(&nbrec_lb->options,
+                                              "address-family", "ipv4"),
+                                 "ipv4")
+                         ? AF_INET
+                         : AF_INET6;
+
+    lb->nlb = nbrec_lb;
+    lb->proto = is_udp ? "udp" : is_sctp ? "sctp" : "tcp";
+    lb->n_vips = smap_count(&nbrec_lb->vips);
+    lb->vips = xcalloc(lb->n_vips, sizeof *lb->vips);
+    lb->vips_nb = xcalloc(lb->n_vips, sizeof *lb->vips_nb);
+    smap_init(&lb->template_vips);
+    lb->controller_event = smap_get_bool(&nbrec_lb->options, "event", false);
+
+    bool routable = smap_get_bool(&nbrec_lb->options, "add_route", false);
+    lb->routable = ovn_lb_get_routable_mode(nbrec_lb, routable, template);
+
+    lb->skip_snat = smap_get_bool(&nbrec_lb->options, "skip_snat", false);
+    lb->template = template;
+
+    const char *mode = smap_get(&nbrec_lb->options, "neighbor_responder");
+    lb->neigh_mode = ovn_lb_get_neigh_mode(nbrec_lb, mode, template);
+
+    uint32_t affinity_timeout =
+        smap_get_uint(&nbrec_lb->options, "affinity_timeout", 0);
+    if (affinity_timeout > UINT16_MAX) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+        VLOG_WARN_RL(&rl, "max affinity_timeout timeout value is %u",
+                     UINT16_MAX);
+        affinity_timeout = UINT16_MAX;
+    }
+    lb->affinity_timeout = affinity_timeout;
+
+    sset_init(&lb->ips_v4);
+    sset_init(&lb->ips_v6);
+    struct smap_node *node;
+    size_t n_vips = 0;
+
+    SMAP_FOR_EACH (node, &nbrec_lb->vips) {
+        struct ovn_lb_vip *lb_vip = &lb->vips[n_vips];
+        struct ovn_northd_lb_vip *lb_vip_nb = &lb->vips_nb[n_vips];
+
+        char *error = ovn_lb_vip_init(lb_vip, node->key, node->value,
+                                      template, address_family);
+        if (error) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "Failed to initialize LB VIP: %s", error);
+            ovn_lb_vip_destroy(lb_vip);
+            free(error);
+            continue;
+        }
+        lb_vip->empty_backend_rej = smap_get_bool(&nbrec_lb->options,
+                                                  "reject", false);
+        ovn_northd_lb_vip_init(lb_vip_nb, lb_vip, nbrec_lb,
+                               node->key, node->value, template);
+        if (lb_vip_nb->lb_health_check) {
+            lb->health_checks = true;
+        }
+
+        if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
+            sset_add(&lb->ips_v4, lb_vip->vip_str);
+        } else {
+            sset_add(&lb->ips_v6, lb_vip->vip_str);
+        }
+
+        if (lb->template && address_family == AF_INET6) {
+            smap_add_nocopy(&lb->template_vips,
+                            ovn_lb_vip6_template_format_internal(lb_vip),
+                            xstrdup(node->value));
+        }
+        n_vips++;
+
+        if (lb_vip_nb->lb_health_check) {
+            ovn_lb_vip_backends_health_check_init(lb, lb_vip, lb_vip_nb);
+        }
+    }
+
+    /* It's possible that parsing VIPs fails.  Update the lb->n_vips to the
+     * correct value.
+     */
+    lb->n_vips = n_vips;
+
+    if (nbrec_lb->n_selection_fields) {
+        char *proto = NULL;
+        if (nbrec_lb->protocol && nbrec_lb->protocol[0]) {
+            proto = nbrec_lb->protocol;
+        }
+
+        struct ds sel_fields = DS_EMPTY_INITIALIZER;
+        for (size_t i = 0; i < lb->nlb->n_selection_fields; i++) {
+            char *field = lb->nlb->selection_fields[i];
+            if (!strcmp(field, "tp_src") && proto) {
+                ds_put_format(&sel_fields, "%s_src,", proto);
+            } else if (!strcmp(field, "tp_dst") && proto) {
+                ds_put_format(&sel_fields, "%s_dst,", proto);
+            } else {
+                ds_put_format(&sel_fields, "%s,", field);
+            }
+        }
+        ds_chomp(&sel_fields, ',');
+        lb->selection_fields = ds_steal_cstr(&sel_fields);
+    }
+}
+
+struct ovn_northd_lb *
+ovn_northd_lb_create(const struct nbrec_load_balancer *nbrec_lb)
+{
+    struct ovn_northd_lb *lb = xzalloc(sizeof *lb);
+    ovn_northd_lb_init(lb, nbrec_lb);
+    return lb;
+}
+
+struct ovn_northd_lb *
+ovn_northd_lb_find(const struct hmap *lbs, const struct uuid *uuid)
+{
+    struct ovn_northd_lb *lb;
+    size_t hash = uuid_hash(uuid);
+    HMAP_FOR_EACH_WITH_HASH (lb, hmap_node, hash, lbs) {
+        if (uuid_equals(&lb->nlb->header_.uuid, uuid)) {
+            return lb;
+        }
+    }
+    return NULL;
+}
+
+const struct smap *
+ovn_northd_lb_get_vips(const struct ovn_northd_lb *lb)
+{
+    if (!smap_is_empty(&lb->template_vips)) {
+        return &lb->template_vips;
+    }
+    return &lb->nlb->vips;
+}
+
+static void
+ovn_northd_lb_cleanup(struct ovn_northd_lb *lb)
+{
+    for (size_t i = 0; i < lb->n_vips; i++) {
+        ovn_lb_vip_destroy(&lb->vips[i]);
+        ovn_northd_lb_vip_destroy(&lb->vips_nb[i]);
+    }
+    free(lb->vips);
+    free(lb->vips_nb);
+    lb->vips = NULL;
+    lb->vips_nb = NULL;
+    smap_destroy(&lb->template_vips);
+    sset_destroy(&lb->ips_v4);
+    sset_destroy(&lb->ips_v6);
+    free(lb->selection_fields);
+    lb->selection_fields = NULL;
+    lb->health_checks = false;
+}
+
+void
+ovn_northd_lb_destroy(struct ovn_northd_lb *lb)
+{
+    ovn_northd_lb_cleanup(lb);
+    free(lb);
+}
+
+void
+ovn_northd_lb_reinit(struct ovn_northd_lb *lb,
+                     const struct nbrec_load_balancer *nbrec_lb)
+{
+    ovn_northd_lb_cleanup(lb);
+    ovn_northd_lb_init(lb, nbrec_lb);
+}
+
+static void
+ovn_lb_group_init(struct ovn_lb_group *lb_group,
+                  const struct nbrec_load_balancer_group *nbrec_lb_group,
+                  const struct hmap *lbs)
+{
+    lb_group->n_lbs = nbrec_lb_group->n_load_balancer;
+    lb_group->lbs = xmalloc(lb_group->n_lbs * sizeof *lb_group->lbs);
+    lb_group->lb_ips = ovn_lb_ip_set_create();
+
+    for (size_t i = 0; i < nbrec_lb_group->n_load_balancer; i++) {
+        const struct uuid *lb_uuid =
+            &nbrec_lb_group->load_balancer[i]->header_.uuid;
+        lb_group->lbs[i] = ovn_northd_lb_find(lbs, lb_uuid);
+        lb_group->has_routable_lb |= lb_group->lbs[i]->routable;
+    }
+}
+
+/* Constructs a new 'struct ovn_lb_group' object from the Nb LB Group record
+ * and an array of 'struct ovn_northd_lb' objects for its associated
+ * load balancers. */
+struct ovn_lb_group *
+ovn_lb_group_create(const struct nbrec_load_balancer_group *nbrec_lb_group,
+                    const struct hmap *lbs)
+{
+    struct ovn_lb_group *lb_group = xzalloc(sizeof *lb_group);
+    lb_group->uuid = nbrec_lb_group->header_.uuid;
+    ovn_lb_group_init(lb_group, nbrec_lb_group, lbs);
+    return lb_group;
+}
+
+static void
+ovn_lb_group_cleanup(struct ovn_lb_group *lb_group)
+{
+    ovn_lb_ip_set_destroy(lb_group->lb_ips);
+    lb_group->lb_ips = NULL;
+    lb_group->has_routable_lb = false;
+    free(lb_group->lbs);
+}
+
+void
+ovn_lb_group_destroy(struct ovn_lb_group *lb_group)
+{
+    if (!lb_group) {
+        return;
+    }
+
+    ovn_lb_group_cleanup(lb_group);
+    free(lb_group);
+}
+
+void
+ovn_lb_group_reinit(struct ovn_lb_group *lb_group,
+                    const struct nbrec_load_balancer_group *nbrec_lb_group,
+                    const struct hmap *lbs)
+{
+    ovn_lb_group_cleanup(lb_group);
+    ovn_lb_group_init(lb_group, nbrec_lb_group, lbs);
+}
+
+struct ovn_lb_group *
+ovn_lb_group_find(const struct hmap *lb_groups, const struct uuid *uuid)
+{
+    struct ovn_lb_group *lb_group;
+    size_t hash = uuid_hash(uuid);
+
+    HMAP_FOR_EACH_WITH_HASH (lb_group, hmap_node, hash, lb_groups) {
+        if (uuid_equals(&lb_group->uuid, uuid)) {
+            return lb_group;
+        }
+    }
+    return NULL;
+}
+
+void
+build_lrouter_lb_ips(struct ovn_lb_ip_set *lb_ips,
+                     const struct ovn_northd_lb *lb)
+{
+    add_ips_to_lb_ip_set(lb_ips, lb->routable, &lb->ips_v4, &lb->ips_v6);
+}
+
+void
+add_ips_to_lb_ip_set(struct ovn_lb_ip_set *lb_ips,
+                     bool is_routable,
+                     const struct sset *lb_ips_v4,
+                     const struct sset *lb_ips_v6)
+{
+    const char *ip_address;
+
+    SSET_FOR_EACH (ip_address, lb_ips_v4) {
+        sset_add(&lb_ips->ips_v4, ip_address);
+        if (is_routable) {
+            sset_add(&lb_ips->ips_v4_routable, ip_address);
+        }
+    }
+    SSET_FOR_EACH (ip_address, lb_ips_v6) {
+        sset_add(&lb_ips->ips_v6, ip_address);
+        if (is_routable) {
+            sset_add(&lb_ips->ips_v6_routable, ip_address);
+        }
+    }
+}
+
+void
+remove_ips_from_lb_ip_set(struct ovn_lb_ip_set *lb_ips,
+                          bool is_routable,
+                          const struct sset *lb_ips_v4,
+                          const struct sset *lb_ips_v6)
+{
+    const char *ip_address;
+
+    SSET_FOR_EACH (ip_address, lb_ips_v4) {
+        sset_find_and_delete(&lb_ips->ips_v4, ip_address);
+        if (is_routable) {
+            sset_find_and_delete(&lb_ips->ips_v4_routable, ip_address);
+        }
+    }
+    SSET_FOR_EACH (ip_address, lb_ips_v6) {
+        sset_find_and_delete(&lb_ips->ips_v6, ip_address);
+        if (is_routable) {
+            sset_find_and_delete(&lb_ips->ips_v6_routable, ip_address);
+        }
+    }
+}
+
+/* lb datapaths functions */
+struct  ovn_lb_datapaths *
+ovn_lb_datapaths_create(const struct ovn_northd_lb *lb, size_t n_ls_datapaths,
+                        size_t n_lr_datapaths)
+{
+    struct ovn_lb_datapaths *lb_dps = xzalloc(sizeof *lb_dps);
+    lb_dps->lb = lb;
+    lb_dps->nb_ls_map = bitmap_allocate(n_ls_datapaths);
+    lb_dps->nb_lr_map = bitmap_allocate(n_lr_datapaths);
+
+    return lb_dps;
+}
+
+void
+ovn_lb_datapaths_destroy(struct ovn_lb_datapaths *lb_dps)
+{
+    bitmap_free(lb_dps->nb_lr_map);
+    bitmap_free(lb_dps->nb_ls_map);
+    free(lb_dps);
+}
+
+void
+ovn_lb_datapaths_add_lr(struct ovn_lb_datapaths *lb_dps, size_t n,
+                        struct ovn_datapath **ods)
+{
+    for (size_t i = 0; i < n; i++) {
+        if (!bitmap_is_set(lb_dps->nb_lr_map, ods[i]->index)) {
+            bitmap_set1(lb_dps->nb_lr_map, ods[i]->index);
+            lb_dps->n_nb_lr++;
+        }
+    }
+}
+
+void
+ovn_lb_datapaths_add_ls(struct ovn_lb_datapaths *lb_dps, size_t n,
+                        struct ovn_datapath **ods)
+{
+    for (size_t i = 0; i < n; i++) {
+        if (!bitmap_is_set(lb_dps->nb_ls_map, ods[i]->index)) {
+            bitmap_set1(lb_dps->nb_ls_map, ods[i]->index);
+            lb_dps->n_nb_ls++;
+        }
+    }
+}
+
+struct ovn_lb_datapaths *
+ovn_lb_datapaths_find(const struct hmap *lb_dps_map,
+                      const struct uuid *lb_uuid)
+{
+    struct ovn_lb_datapaths *lb_dps;
+    size_t hash = uuid_hash(lb_uuid);
+    HMAP_FOR_EACH_WITH_HASH (lb_dps, hmap_node, hash, lb_dps_map) {
+        if (uuid_equals(&lb_dps->lb->nlb->header_.uuid, lb_uuid)) {
+            return lb_dps;
+        }
+    }
+    return NULL;
+}
+
+struct ovn_lb_group_datapaths *
+ovn_lb_group_datapaths_create(const struct ovn_lb_group *lb_group,
+                              size_t max_ls_datapaths,
+                              size_t max_lr_datapaths)
+{
+    struct ovn_lb_group_datapaths *lb_group_dps =
+        xzalloc(sizeof *lb_group_dps);
+    lb_group_dps->lb_group = lb_group;
+    lb_group_dps->ls = xmalloc(max_ls_datapaths * sizeof *lb_group_dps->ls);
+    lb_group_dps->lr = xmalloc(max_lr_datapaths * sizeof *lb_group_dps->lr);
+
+    return lb_group_dps;
+}
+
+void
+ovn_lb_group_datapaths_destroy(struct ovn_lb_group_datapaths *lb_group_dps)
+{
+    free(lb_group_dps->ls);
+    free(lb_group_dps->lr);
+    free(lb_group_dps);
+}
+
+struct ovn_lb_group_datapaths *
+ovn_lb_group_datapaths_find(const struct hmap *lb_group_dps_map,
+                            const struct uuid *lb_group_uuid)
+{
+    struct ovn_lb_group_datapaths *lb_group_dps;
+    size_t hash = uuid_hash(lb_group_uuid);
+
+    HMAP_FOR_EACH_WITH_HASH (lb_group_dps, hmap_node, hash, lb_group_dps_map) {
+        if (uuid_equals(&lb_group_dps->lb_group->uuid, lb_group_uuid)) {
+            return lb_group_dps;
+        }
+    }
+    return NULL;
+}
diff --git a/northd/lb.h b/northd/lb.h
new file mode 100644
index 0000000000..00f81c3533
--- /dev/null
+++ b/northd/lb.h
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2024, Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at:
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef OVN_NORTHD_LB_H
+#define OVN_NORTHD_LB_H 1
+
+#include "openvswitch/hmap.h"
+#include "uuid.h"
+
+#include "lib/lb.h"
+
+struct nbrec_load_balancer;
+struct nbrec_load_balancer_group;
+struct ovn_datapath;
+
+enum lb_neighbor_responder_mode {
+    LB_NEIGH_RESPOND_REACHABLE,
+    LB_NEIGH_RESPOND_ALL,
+    LB_NEIGH_RESPOND_NONE,
+};
+
+/* The "routable" ssets are subsets of the load balancer IPs for which IP
+ * routes and ARP resolution flows are automatically added. */
+struct ovn_lb_ip_set {
+    struct sset ips_v4;
+    struct sset ips_v4_routable;
+    struct sset ips_v4_reachable;
+    struct sset ips_v6;
+    struct sset ips_v6_routable;
+    struct sset ips_v6_reachable;
+};
+
+struct ovn_lb_ip_set *ovn_lb_ip_set_create(void);
+void ovn_lb_ip_set_destroy(struct ovn_lb_ip_set *);
+struct ovn_lb_ip_set *ovn_lb_ip_set_clone(struct ovn_lb_ip_set *);
+
+struct ovn_northd_lb {
+    struct hmap_node hmap_node;
+
+    const struct nbrec_load_balancer *nlb; /* May be NULL. */
+    const char *proto;
+    char *selection_fields;
+    struct ovn_lb_vip *vips;
+    struct ovn_northd_lb_vip *vips_nb;
+    struct smap template_vips; /* Slightly changed template VIPs, populated
+                                * if needed.  Until now it's only required
+                                * for IPv6 template load balancers. */
+    size_t n_vips;
+
+    enum lb_neighbor_responder_mode neigh_mode;
+    bool controller_event;
+    bool routable;
+    bool skip_snat;
+    bool template;
+    uint16_t affinity_timeout;
+
+    struct sset ips_v4;
+    struct sset ips_v6;
+
+    /* Indicates if the load balancer has health checks configured. */
+    bool health_checks;
+};
+
+/* ovn-northd specific backend information. */
+struct ovn_northd_lb_vip {
+    char *backend_ips;
+    struct ovn_northd_lb_backend *backends_nb;
+    size_t n_backends;
+
+    struct nbrec_load_balancer_health_check *lb_health_check;
+};
+
+struct ovn_northd_lb_backend {
+    bool health_check;
+    char *logical_port; /* Logical port to which the ip belong to. */
+    char *svc_mon_src_ip; /* Source IP to use for monitoring. */
+};
+
+struct ovn_northd_lb *ovn_northd_lb_create(const struct nbrec_load_balancer *);
+struct ovn_northd_lb *ovn_northd_lb_find(const struct hmap *,
+                                         const struct uuid *);
+const struct smap *ovn_northd_lb_get_vips(const struct ovn_northd_lb *);
+void ovn_northd_lb_destroy(struct ovn_northd_lb *);
+void ovn_northd_lb_reinit(struct ovn_northd_lb *,
+                          const struct nbrec_load_balancer *);
+
+void build_lrouter_lb_ips(struct ovn_lb_ip_set *,
+                          const struct ovn_northd_lb *);
+void add_ips_to_lb_ip_set(struct ovn_lb_ip_set *lb_ips,
+                          bool is_routable,
+                          const struct sset *lb_ips_v4,
+                          const struct sset *lb_ips_v6);
+void remove_ips_from_lb_ip_set(struct ovn_lb_ip_set *lb_ips,
+                               bool is_routable,
+                               const struct sset *lb_ips_v4,
+                               const struct sset *lb_ips_v6);
+
+struct ovn_lb_group {
+    struct hmap_node hmap_node;
+    struct uuid uuid;
+    size_t n_lbs;
+    struct ovn_northd_lb **lbs;
+    struct ovn_lb_ip_set *lb_ips;
+    bool has_routable_lb;
+};
+
+struct ovn_lb_group *ovn_lb_group_create(
+    const struct nbrec_load_balancer_group *,
+    const struct hmap *lbs);
+void ovn_lb_group_destroy(struct ovn_lb_group *lb_group);
+struct ovn_lb_group *ovn_lb_group_find(const struct hmap *lb_groups,
+                                       const struct uuid *);
+void ovn_lb_group_reinit(
+    struct ovn_lb_group *lb_group,
+    const struct nbrec_load_balancer_group *,
+    const struct hmap *lbs);
+
+struct ovn_lb_datapaths {
+    struct hmap_node hmap_node;
+
+    const struct ovn_northd_lb *lb;
+    size_t n_nb_ls;
+    unsigned long *nb_ls_map;
+
+    size_t n_nb_lr;
+    unsigned long *nb_lr_map;
+};
+
+struct ovn_lb_datapaths *ovn_lb_datapaths_create(const struct ovn_northd_lb *,
+                                                 size_t n_ls_datapaths,
+                                                 size_t n_lr_datapaths);
+struct ovn_lb_datapaths *ovn_lb_datapaths_find(const struct hmap *,
+                                               const struct uuid *);
+void ovn_lb_datapaths_destroy(struct ovn_lb_datapaths *);
+
+void ovn_lb_datapaths_add_lr(struct ovn_lb_datapaths *, size_t n,
+                             struct ovn_datapath **);
+void ovn_lb_datapaths_add_ls(struct ovn_lb_datapaths *, size_t n,
+                             struct ovn_datapath **);
+
+struct ovn_lb_group_datapaths {
+    struct hmap_node hmap_node;
+
+    const struct ovn_lb_group *lb_group;
+
+    /* Datapaths to which 'lb_group' is applied. */
+    size_t n_ls;
+    struct ovn_datapath **ls;
+    size_t n_lr;
+    struct ovn_datapath **lr;
+};
+
+struct ovn_lb_group_datapaths *ovn_lb_group_datapaths_create(
+    const struct ovn_lb_group *, size_t max_ls_datapaths,
+    size_t max_lr_datapaths);
+
+void ovn_lb_group_datapaths_destroy(struct ovn_lb_group_datapaths *);
+struct ovn_lb_group_datapaths *ovn_lb_group_datapaths_find(
+    const struct hmap *lb_group_dps, const struct uuid *);
+
+static inline void
+ovn_lb_group_datapaths_add_ls(struct ovn_lb_group_datapaths *lbg_dps, size_t n,
+                               struct ovn_datapath **ods)
+{
+    memcpy(&lbg_dps->ls[lbg_dps->n_ls], ods, n * sizeof *ods);
+    lbg_dps->n_ls += n;
+}
+
+static inline void
+ovn_lb_group_datapaths_add_lr(struct ovn_lb_group_datapaths *lbg_dps,
+                               struct ovn_datapath *lr)
+{
+    lbg_dps->lr[lbg_dps->n_lr++] = lr;
+}
+
+#endif /* OVN_NORTHD_LB_H */
diff --git a/northd/northd.c b/northd/northd.c
index cb53ec9716..00899e3d1a 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -31,6 +31,7 @@
 #include "openvswitch/hmap.h"
 #include "openvswitch/json.h"
 #include "ovn/lex.h"
+#include "lb.h"
 #include "lib/chassis-index.h"
 #include "lib/ip-mcast-index.h"
 #include "lib/static-mac-binding-index.h"
