diff --git a/Knot.files b/Knot.files
index 873e4f9b89c1904839031dbbd9247fd3c777a7b8..ab823637f9a139d368ef2a2366aaca651eb8381e 100644
--- a/Knot.files
+++ b/Knot.files
@@ -212,6 +212,8 @@ src/knot/worker/pool.c
 src/knot/worker/pool.h
 src/knot/worker/queue.c
 src/knot/worker/queue.h
+src/knot/zone/adjust.c
+src/knot/zone/adjust.h
 src/knot/zone/contents.c
 src/knot/zone/contents.h
 src/knot/zone/node.c
diff --git a/src/knot/Makefile.inc b/src/knot/Makefile.inc
index 36e922810266be5d5f91b39e49122c71fcb0f9a7..cb8a6f9fe097d16225bd6881a966b1bb08460717 100644
--- a/src/knot/Makefile.inc
+++ b/src/knot/Makefile.inc
@@ -147,6 +147,8 @@ libknotd_la_SOURCES = \
 	knot/worker/pool.h			\
 	knot/worker/queue.c			\
 	knot/worker/queue.h			\
+	knot/zone/adjust.c			\
+	knot/zone/adjust.h			\
 	knot/zone/contents.c			\
 	knot/zone/contents.h			\
 	knot/zone/node.c			\
diff --git a/src/knot/dnssec/zone-events.c b/src/knot/dnssec/zone-events.c
index 8928d9a6451bfa85989ef8671982001a36aced2b..069bb4d3df364100027b07044baaa29cd7b8fa82 100644
--- a/src/knot/dnssec/zone-events.c
+++ b/src/knot/dnssec/zone-events.c
@@ -50,11 +50,6 @@ static int sign_init(zone_contents_t *zone, zone_sign_flags_t flags, zone_sign_r
 		}
 	}
 
-	r = zone_contents_adjust_full(zone);
-	if (r != KNOT_EOK) {
-		return r;
-	}
-
 	// update policy based on the zone content
 	update_policy_from_zone(ctx->policy, zone);
 
diff --git a/src/knot/dnssec/zone-sign.c b/src/knot/dnssec/zone-sign.c
index 173cb2b42d3c064c2bebe669932985438ac7246a..7f47d4b1b1f280c846a03ae01430bea54fafae92 100644
--- a/src/knot/dnssec/zone-sign.c
+++ b/src/knot/dnssec/zone-sign.c
@@ -26,6 +26,7 @@
 #include "knot/dnssec/key_records.h"
 #include "knot/dnssec/rrset-sign.h"
 #include "knot/dnssec/zone-sign.h"
+#include "knot/zone/adjust.h"
 #include "libknot/libknot.h"
 #include "contrib/dynarray.h"
 #include "contrib/macros.h"
@@ -1197,8 +1198,7 @@ int knot_zone_sign_update(zone_update_t *update,
 
 	int ret = KNOT_EOK;
 
-
-	ret = apply_prepare_to_sign(update->a_ctx);
+	ret = zone_adjust_update(update, adjust_cb_flags_and_additionals, adjust_cb_nsec3_flags);
 	if (ret != KNOT_EOK) {
 		return ret;
 	}
diff --git a/src/knot/events/handlers/refresh.c b/src/knot/events/handlers/refresh.c
index 37b638800199e19d42b33b6e6398b1b28c101c72..78245a93a4cb9bd912eb1c4b986e78d41f840a88 100644
--- a/src/knot/events/handlers/refresh.c
+++ b/src/knot/events/handlers/refresh.c
@@ -212,12 +212,7 @@ static int axfr_finalize(struct refresh_data *data)
 {
 	zone_contents_t *new_zone = data->axfr.zone;
 
-	int ret = zone_contents_adjust_full(new_zone);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	ret = xfr_validate(new_zone, data);
+	int ret = xfr_validate(new_zone, data);
 	if (ret != KNOT_EOK) {
 		return ret;
 	}
diff --git a/src/knot/updates/apply.c b/src/knot/updates/apply.c
index f9c8b3ddd02760d0cf09645a1f57c16b17d8d43e..722687672b5d7431ef1c1279cfea8afd6c27565d 100644
--- a/src/knot/updates/apply.c
+++ b/src/knot/updates/apply.c
@@ -218,16 +218,30 @@ static int apply_single(apply_ctx_t *ctx, const changeset_t *chset)
 
 /* ------------------------------- API -------------------------------------- */
 
-void apply_init_ctx(apply_ctx_t *ctx, zone_contents_t *contents, uint32_t flags)
+int apply_init_ctx(apply_ctx_t *ctx, zone_contents_t *contents, uint32_t flags)
 {
-	assert(ctx);
+	if (ctx == NULL) {
+		return KNOT_EINVAL;
+	}
 
 	ctx->contents = contents;
 
 	init_list(&ctx->old_data);
 	init_list(&ctx->new_data);
 
+	ctx->node_ptrs = zone_tree_create();
+	if (ctx->node_ptrs == NULL) {
+		return KNOT_ENOMEM;
+	}
+	ctx->nsec3_ptrs = zone_tree_create();
+	if (ctx->nsec3_ptrs == NULL) {
+		zone_tree_free(&ctx->node_ptrs);
+		return KNOT_ENOMEM;
+	}
+
 	ctx->flags = flags;
+
+	return KNOT_EOK;
 }
 
 int apply_prepare_zone_copy(zone_contents_t *old_contents,
@@ -265,6 +279,8 @@ int apply_add_rr(apply_ctx_t *ctx, const knot_rrset_t *rr)
 	if (node == NULL) {
 		return KNOT_ENOMEM;
 	}
+	zone_tree_insert(knot_rrset_is_nsec3rel(rr) ? ctx->nsec3_ptrs : ctx->node_ptrs, node);
+	// re-inserting makes no harm
 
 	knot_rrset_t changed_rrset = node_rrset(node, rr->type);
 	if (!knot_rrset_empty(&changed_rrset)) {
@@ -327,8 +343,9 @@ int apply_remove_rr(apply_ctx_t *ctx, const knot_rrset_t *rr)
 		return KNOT_EOK;
 	}
 
-	zone_tree_t *tree = knot_rrset_is_nsec3rel(rr) ?
-	                    contents->nsec3_nodes : contents->nodes;
+	bool nsec3 = knot_rrset_is_nsec3rel(rr);
+	zone_tree_insert(nsec3 ? ctx->nsec3_ptrs : ctx->node_ptrs, node);
+	zone_tree_t *tree = nsec3 ? contents->nsec3_nodes : contents->nodes;
 
 	knot_rrset_t removed_rrset = node_rrset(node, rr->type);
 	knot_rdata_t *old_data = removed_rrset.rrs.rdata;
@@ -364,6 +381,7 @@ int apply_remove_rr(apply_ctx_t *ctx, const knot_rrset_t *rr)
 		node_remove_rdataset(node, rr->type);
 		// If node is empty now, delete it from zone tree.
 		if (node->rrset_count == 0 && node != contents->apex) {
+			zone_tree_remove_node(nsec3 ? ctx->nsec3_ptrs: ctx->node_ptrs, node->owner);
 			zone_tree_delete_empty(tree, node);
 		}
 	}
@@ -393,11 +411,6 @@ int apply_replace_soa(apply_ctx_t *ctx, const changeset_t *chset)
 	return apply_add_rr(ctx, chset->soa_to);
 }
 
-int apply_prepare_to_sign(apply_ctx_t *ctx)
-{
-	return zone_contents_adjust_pointers(ctx->contents);
-}
-
 int apply_changesets_directly(apply_ctx_t *ctx, list_t *chsets)
 {
 	if (ctx == NULL || ctx->contents == NULL || chsets == NULL) {
@@ -412,7 +425,7 @@ int apply_changesets_directly(apply_ctx_t *ctx, list_t *chsets)
 		}
 	}
 
-	return zone_contents_adjust_full(ctx->contents);
+	return KNOT_EOK;
 }
 
 int apply_changeset_directly(apply_ctx_t *ctx, const changeset_t *ch)
@@ -427,26 +440,18 @@ int apply_changeset_directly(apply_ctx_t *ctx, const changeset_t *ch)
 		return ret;
 	}
 
-	ret = zone_contents_adjust_full(ctx->contents);
-	if (ret != KNOT_EOK) {
-		update_rollback(ctx);
-		return ret;
-	}
-
 	return KNOT_EOK;
 }
 
-int apply_finalize(apply_ctx_t *ctx)
-{
-	return zone_contents_adjust_full(ctx->contents);
-}
-
 void update_cleanup(apply_ctx_t *ctx)
 {
 	if (ctx == NULL) {
 		return;
 	}
 
+	zone_tree_free(&ctx->node_ptrs);
+	zone_tree_free(&ctx->nsec3_ptrs);
+
 	// Delete old RR data
 	rrs_list_clear(&ctx->old_data, NULL);
 	init_list(&ctx->old_data);
@@ -461,6 +466,9 @@ void update_rollback(apply_ctx_t *ctx)
 		return;
 	}
 
+	zone_tree_free(&ctx->node_ptrs);
+	zone_tree_free(&ctx->nsec3_ptrs);
+
 	// Delete new RR data
 	rrs_list_clear(&ctx->new_data, NULL);
 	init_list(&ctx->new_data);
diff --git a/src/knot/updates/apply.h b/src/knot/updates/apply.h
index 14538d21e5e54cf7cd68534e5f7001e9fabc0be6..4747081289c095ffa3233218b61a16f12fd9c5c2 100644
--- a/src/knot/updates/apply.h
+++ b/src/knot/updates/apply.h
@@ -28,6 +28,8 @@ struct apply_ctx {
 	zone_contents_t *contents;
 	list_t old_data;          /*!< Old data, to be freed after successful update. */
 	list_t new_data;          /*!< New data, to be freed after failed update. */
+	zone_tree_t *node_ptrs;   /*!< Just pointers to the affected nodes in contents. */
+	zone_tree_t *nsec3_ptrs;  /*!< The same for NSEC3 nodes. */
 	uint32_t flags;
 };
 
@@ -39,8 +41,10 @@ typedef struct apply_ctx apply_ctx_t;
  * \param ctx       Context to be initialized.
  * \param contents  Zone contents to apply changes onto.
  * \param flags     Flags to control the application process.
+ *
+ * \return KNOT_E*
  */
-void apply_init_ctx(apply_ctx_t *ctx, zone_contents_t *contents, uint32_t flags);
+int apply_init_ctx(apply_ctx_t *ctx, zone_contents_t *contents, uint32_t flags);
 
 /*!
  * \brief Creates a shallow zone contents copy.
@@ -83,17 +87,6 @@ int apply_remove_rr(apply_ctx_t *ctx, const knot_rrset_t *rr);
  */
 int apply_replace_soa(apply_ctx_t *ctx, const changeset_t *ch);
 
-/*!
- * \brief Prepares the new zone contents for signing.
- *
- * Adjusted pointers are required for DNSSEC.
- *
- * \param ctx  Apply context.
- *
- * \return KNOT_E*
- */
-int apply_prepare_to_sign(apply_ctx_t *ctx);
-
 /*!
  * \brief Applies changesets directly to the zone, without copying it.
  *
@@ -116,17 +109,6 @@ int apply_changesets_directly(apply_ctx_t *ctx, list_t *chsets);
  */
 int apply_changeset_directly(apply_ctx_t *ctx, const changeset_t *ch);
 
-/*!
- * \brief Finalizes the zone contents for publishing.
- *
- * Fully adjusts the zone.
- *
- * \param ctx  Apply context.
- *
- * \return KNOT_E*
- */
-int apply_finalize(apply_ctx_t *ctx);
-
 /*!
  * \brief Cleanups successful zone update.
  *
diff --git a/src/knot/updates/zone-update.c b/src/knot/updates/zone-update.c
index cd5d47696f68b08b36b3da694c7a22feb010c902..2007404a0f59e22b5b841f554023469afb843337 100644
--- a/src/knot/updates/zone-update.c
+++ b/src/knot/updates/zone-update.c
@@ -17,6 +17,7 @@
 #include "knot/common/log.h"
 #include "knot/dnssec/zone-events.h"
 #include "knot/updates/zone-update.h"
+#include "knot/zone/adjust.h"
 #include "knot/zone/serial.h"
 #include "knot/zone/zone-diff.h"
 #include "contrib/mempattern.h"
@@ -50,7 +51,11 @@ static int init_incremental(zone_update_t *update, zone_t *zone, zone_contents_t
 	}
 
 	uint32_t apply_flags = update->flags & UPDATE_STRICT ? APPLY_STRICT : 0;
-	apply_init_ctx(update->a_ctx, update->new_cont, apply_flags);
+	ret = apply_init_ctx(update->a_ctx, update->new_cont, apply_flags);
+	if (ret != KNOT_EOK) {
+		changeset_clear(&update->change);
+		return ret;
+	}
 
 	/* Copy base SOA RR. */
 	update->change.soa_from =
@@ -73,7 +78,11 @@ static int init_full(zone_update_t *update, zone_t *zone)
 
 	update->new_cont_deep_copy = true;
 
-	apply_init_ctx(update->a_ctx, update->new_cont, 0);
+	int ret = apply_init_ctx(update->a_ctx, update->new_cont, 0);
+	if (ret != KNOT_EOK) {
+		zone_contents_free(update->new_cont);
+		return ret;
+	}
 
 	return KNOT_EOK;
 }
@@ -224,7 +233,12 @@ int zone_update_from_contents(zone_update_t *update, zone_t *zone_without_conten
 	}
 
 	uint32_t apply_flags = update->flags & UPDATE_STRICT ? APPLY_STRICT : 0;
-	apply_init_ctx(update->a_ctx, update->new_cont, apply_flags);
+	int ret = apply_init_ctx(update->a_ctx, update->new_cont, apply_flags);
+	if (ret != KNOT_EOK) {
+		changeset_clear(&update->change);
+		free(update->a_ctx);
+		return ret;
+	}
 
 	return KNOT_EOK;
 }
@@ -584,23 +598,13 @@ int zone_update_increment_soa(zone_update_t *update, conf_t *conf)
 	return set_new_soa(update, conf_opt(&val));
 }
 
-static int commit_incremental(conf_t *conf, zone_update_t *update,
-                              zone_contents_t **contents_out)
+static int commit_incremental(conf_t *conf, zone_update_t *update)
 {
 	assert(update);
-	assert(contents_out);
-
-	if (changeset_empty(&update->change)) {
-		changeset_clear(&update->change);
-		if (update->zone->contents == NULL || update->new_cont_deep_copy) {
-			*contents_out = update->new_cont;
-		}
-		return KNOT_EOK;
-	}
 
 	zone_contents_t *new_contents = update->new_cont;
 	int ret = KNOT_EOK;
-	if (zone_update_to(update) == NULL) {
+	if (zone_update_to(update) == NULL && !changeset_empty(&update->change)) {
 		/* No SOA in the update, create one according to the current policy */
 		ret = zone_update_increment_soa(update, conf);
 		if (ret != KNOT_EOK) {
@@ -609,7 +613,7 @@ static int commit_incremental(conf_t *conf, zone_update_t *update,
 		}
 	}
 
-	ret = apply_finalize(update->a_ctx);
+	ret = zone_adjust_full(new_contents);
 	if (ret != KNOT_EOK) {
 		zone_update_clear(update);
 		return ret;
@@ -617,22 +621,19 @@ static int commit_incremental(conf_t *conf, zone_update_t *update,
 
 	/* Write changes to journal if all went well. */
 	conf_val_t val = conf_zone_get(conf, C_JOURNAL_CONTENT, update->zone->name);
-	if (conf_opt(&val) != JOURNAL_CONTENT_NONE) {
+	if (conf_opt(&val) != JOURNAL_CONTENT_NONE && !changeset_empty(&update->change)) {
 		ret = zone_change_store(conf, update->zone, &update->change);
 		if (ret != KNOT_EOK) {
 			return ret;
 		}
 	}
 
-	*contents_out = new_contents;
-
 	return KNOT_EOK;
 }
 
-static int commit_full(conf_t *conf, zone_update_t *update, zone_contents_t **contents_out)
+static int commit_full(conf_t *conf, zone_update_t *update)
 {
 	assert(update);
-	assert(contents_out);
 
 	/* Check if we have SOA. We might consider adding full semantic check here.
 	 * But if we wanted full sem-check I'd consider being it controlled by a flag
@@ -641,7 +642,7 @@ static int commit_full(conf_t *conf, zone_update_t *update, zone_contents_t **co
 		return KNOT_ESEMCHECK;
 	}
 
-	int ret = zone_contents_adjust_full(update->new_cont);
+	int ret = zone_adjust_full(update->new_cont);
 	if (ret != KNOT_EOK) {
 		zone_update_clear(update);
 		return ret;
@@ -655,8 +656,6 @@ static int commit_full(conf_t *conf, zone_update_t *update, zone_contents_t **co
 		ret = zone_changes_clear(conf, update->zone);
 	}
 
-	*contents_out = update->new_cont;
-
 	return ret;
 }
 
@@ -703,26 +702,25 @@ int zone_update_commit(conf_t *conf, zone_update_t *update)
 	}
 
 	int ret = KNOT_EOK;
-	zone_contents_t *new_contents = NULL;
 	if (update->flags & UPDATE_INCREMENTAL) {
-		ret = commit_incremental(conf, update, &new_contents);
+		if (changeset_empty(&update->change) &&
+		    update->zone->contents != NULL && !update->new_cont_deep_copy) {
+			changeset_clear(&update->change);
+			return KNOT_EOK;
+		}
+		ret = commit_incremental(conf, update);
 	} else {
-		ret = commit_full(conf, update, &new_contents);
+		ret = commit_full(conf, update);
 	}
 	if (ret != KNOT_EOK) {
 		return ret;
 	}
 
-	/* If there is anything to change. */
-	if (new_contents == NULL) {
-		return KNOT_EOK;
-	}
-
 	/* Check the zone size. */
 	conf_val_t val = conf_zone_get(conf, C_MAX_ZONE_SIZE, update->zone->name);
 	size_t size_limit = conf_int(&val);
 
-	if (new_contents->size > size_limit) {
+	if (update->new_cont->size > size_limit) {
 		/* Recoverable error. */
 		return KNOT_EZONESIZE;
 	}
@@ -737,7 +735,7 @@ int zone_update_commit(conf_t *conf, zone_update_t *update)
 
 	/* Switch zone contents. */
 	zone_contents_t *old_contents;
-	old_contents = zone_switch_contents(update->zone, new_contents);
+	old_contents = zone_switch_contents(update->zone, update->new_cont);
 
 	/* Sync RCU. */
 	if (update->flags & UPDATE_FULL) {
diff --git a/src/knot/zone/adjust.c b/src/knot/zone/adjust.c
new file mode 100644
index 0000000000000000000000000000000000000000..c950a57fbcb0d9066791602f28e41c37bb1ad2d0
--- /dev/null
+++ b/src/knot/zone/adjust.c
@@ -0,0 +1,395 @@
+/*  Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "knot/zone/adjust.h"
+
+#include "libdnssec/error.h"
+#include "contrib/macros.h"
+#include "knot/common/log.h"
+#include "knot/dnssec/zone-nsec.h"
+
+int adjust_cb_flags(zone_node_t *node, const zone_contents_t *zone)
+{
+	// clear Removed NSEC flag so that no relicts remain
+	node->flags &= ~NODE_FLAGS_REMOVED_NSEC;
+
+	// check if this node is not a wildcard child of its parent
+	if (knot_dname_is_wildcard(node->owner)) {
+		assert(node->parent != NULL);
+		node->parent->flags |= NODE_FLAGS_WILDCARD_CHILD;
+	}
+
+	// set flags (delegation point, non-authoritative)
+	if (node->parent &&
+	    (node->parent->flags & NODE_FLAGS_DELEG ||
+	     node->parent->flags & NODE_FLAGS_NONAUTH)) {
+		node->flags |= NODE_FLAGS_NONAUTH;
+	} else if (node_rrtype_exists(node, KNOT_RRTYPE_NS) && node != zone->apex) {
+		node->flags |= NODE_FLAGS_DELEG;
+	} else {
+		// Default.
+		node->flags = NODE_FLAGS_AUTH;
+	}
+
+	return KNOT_EOK; // always returns this value :)
+}
+
+int adjust_cb_point_to_nsec3(zone_node_t *node, const zone_contents_t *zone)
+{
+	if (!knot_is_nsec3_enabled(zone)) {
+		node->nsec3_node = NULL;
+		return KNOT_EOK;
+	}
+	node->nsec3_wildcard_prev = NULL;
+	uint8_t nsec3_name[KNOT_DNAME_MAXLEN];
+	int ret = knot_create_nsec3_owner(nsec3_name, sizeof(nsec3_name), node->owner,
+	                                  zone->apex->owner, &zone->nsec3_params);
+	if (ret == KNOT_EOK) {
+		node->nsec3_node = zone_tree_get(zone->nsec3_nodes, nsec3_name);
+	}
+	return ret;
+}
+
+int adjust_cb_wildcard_nsec3(zone_node_t *node, const zone_contents_t *zone)
+{
+	if (!knot_is_nsec3_enabled(zone)) {
+		node->nsec3_wildcard_prev = NULL;
+		return KNOT_EOK;
+	}
+
+	const zone_node_t *ignored;
+	int ret = KNOT_EOK;
+	size_t wildcard_size = knot_dname_size(node->owner) + 2;
+	if (wildcard_size <= KNOT_DNAME_MAXLEN) {
+		assert(wildcard_size > 2);
+		knot_dname_t wildcard[wildcard_size];
+		memcpy(wildcard, "\x01""*", 2);
+		memcpy(wildcard + 2, node->owner, wildcard_size - 2);
+		ret = zone_contents_find_nsec3_for_name(zone, wildcard, &ignored,
+							(const zone_node_t **)&node->nsec3_wildcard_prev);
+		if (ret == ZONE_NAME_FOUND) {
+			node->nsec3_wildcard_prev = NULL;
+			ret = KNOT_EOK;
+		}
+	}
+	return ret;
+}
+
+static bool nsec3_params_match(const knot_rdataset_t *rrs,
+                               const dnssec_nsec3_params_t *params,
+                               size_t rdata_pos)
+{
+	assert(rrs != NULL);
+	assert(params != NULL);
+
+	knot_rdata_t *rdata = knot_rdataset_at(rrs, rdata_pos);
+
+	return (knot_nsec3_alg(rdata) == params->algorithm
+	        && knot_nsec3_iters(rdata) == params->iterations
+	        && knot_nsec3_salt_len(rdata) == params->salt.size
+	        && memcmp(knot_nsec3_salt(rdata), params->salt.data,
+	                  params->salt.size) == 0);
+}
+
+int adjust_cb_nsec3_flags(zone_node_t *node, const zone_contents_t *zone)
+{
+	// check if this node belongs to correct chain
+	const knot_rdataset_t *nsec3_rrs = node_rdataset(node, KNOT_RRTYPE_NSEC3);
+	for (uint16_t i = 0; nsec3_rrs != NULL && i < nsec3_rrs->count; i++) {
+		if (nsec3_params_match(nsec3_rrs, &zone->nsec3_params, i)) {
+			node->flags |= NODE_FLAGS_IN_NSEC3_CHAIN;
+		}
+	}
+	return KNOT_EOK;
+}
+
+/*! \brief Link pointers to additional nodes for this RRSet. */
+static int discover_additionals(const knot_dname_t *owner, struct rr_data *rr_data,
+                                const zone_contents_t *zone)
+{
+	assert(rr_data != NULL);
+
+	/* Drop possible previous additional nodes. */
+	additional_clear(rr_data->additional);
+	rr_data->additional = NULL;
+
+	const knot_rdataset_t *rrs = &rr_data->rrs;
+	uint16_t rdcount = rrs->count;
+
+	uint16_t mandatory_count = 0;
+	uint16_t others_count = 0;
+	glue_t mandatory[rdcount];
+	glue_t others[rdcount];
+
+	/* Scan new additional nodes. */
+	for (uint16_t i = 0; i < rdcount; i++) {
+		knot_rdata_t *rdata = knot_rdataset_at(rrs, i);
+		const knot_dname_t *dname = knot_rdata_name(rdata, rr_data->type);
+		const zone_node_t *node = NULL, *encloser = NULL;
+
+		/* Try to find node for the dname in the RDATA. */
+		zone_contents_find_dname(zone, dname, &node, &encloser, NULL);
+		if (node == NULL && encloser != NULL
+		    && (encloser->flags & NODE_FLAGS_WILDCARD_CHILD)) {
+			/* Find wildcard child in the zone. */
+			node = zone_contents_find_wildcard_child(zone, encloser);
+			assert(node != NULL);
+		}
+
+		if (node == NULL) {
+			continue;
+		}
+
+		glue_t *glue;
+		if ((node->flags & (NODE_FLAGS_DELEG | NODE_FLAGS_NONAUTH)) &&
+		    rr_data->type == KNOT_RRTYPE_NS &&
+		    knot_dname_in_bailiwick(node->owner, owner) >= 0) {
+			glue = &mandatory[mandatory_count++];
+			glue->optional = false;
+		} else {
+			glue = &others[others_count++];
+			glue->optional = true;
+		}
+		glue->node = node;
+		glue->ns_pos = i;
+	}
+
+	/* Store sorted additionals by the type, mandatory first. */
+	size_t total_count = mandatory_count + others_count;
+	if (total_count > 0) {
+		rr_data->additional = malloc(sizeof(additional_t));
+		if (rr_data->additional == NULL) {
+			return KNOT_ENOMEM;
+		}
+		rr_data->additional->count = total_count;
+
+		size_t size = total_count * sizeof(glue_t);
+		rr_data->additional->glues = malloc(size);
+		if (rr_data->additional->glues == NULL) {
+			free(rr_data->additional);
+			return KNOT_ENOMEM;
+		}
+
+		size_t mandatory_size = mandatory_count * sizeof(glue_t);
+		memcpy(rr_data->additional->glues, mandatory, mandatory_size);
+		memcpy(rr_data->additional->glues + mandatory_count, others,
+		       size - mandatory_size);
+	}
+
+	return KNOT_EOK;
+}
+
+int adjust_cb_additionals(zone_node_t *node, const zone_contents_t *zone)
+{
+	/* Lookup additional records for specific nodes. */
+	for(uint16_t i = 0; i < node->rrset_count; ++i) {
+		struct rr_data *rr_data = &node->rrs[i];
+		if (knot_rrtype_additional_needed(rr_data->type)) {
+			int ret = discover_additionals(node->owner, rr_data, zone);
+			if (ret != KNOT_EOK) {
+				return ret;
+			}
+		}
+	}
+	return KNOT_EOK;
+}
+
+int adjust_cb_flags_and_additionals(zone_node_t *node, const zone_contents_t *zone)
+{
+	int ret = adjust_cb_flags(node, zone);
+	if (ret == KNOT_EOK) {
+		ret = adjust_cb_additionals(node, zone);
+	}
+	return ret;
+}
+
+int adjust_cb_flags_and_nsec3(zone_node_t *node, const zone_contents_t *zone)
+{
+	int ret = adjust_cb_flags(node, zone);
+	if (ret == KNOT_EOK) {
+		ret = adjust_cb_point_to_nsec3(node, zone);
+	}
+	return ret;
+}
+
+int adjust_cb_nsec3_and_additionals(zone_node_t *node, const zone_contents_t *zone)
+{
+	int ret = adjust_cb_point_to_nsec3(node, zone);
+	if (ret == KNOT_EOK) {
+		ret = adjust_cb_wildcard_nsec3(node, zone);
+	}
+	if (ret == KNOT_EOK) {
+		ret = adjust_cb_additionals(node, zone);
+	}
+	return ret;
+}
+
+int adjust_cb_void(zone_node_t *node, const zone_contents_t *zone)
+{
+	UNUSED(node);
+	UNUSED(zone);
+	return KNOT_EOK;
+}
+
+typedef struct {
+	zone_node_t *first_node;
+	const zone_contents_t *zone;
+	zone_node_t *previous_node;
+	size_t zone_size;
+	uint32_t zone_max_ttl;
+	adjust_cb_t adjust_cb;
+	bool adjust_prevs;
+} zone_adjust_arg_t;
+
+static int adjust_single(zone_node_t **tnode, void *data)
+{
+	assert(tnode != NULL);
+	assert(data != NULL);
+
+	zone_adjust_arg_t *args = (zone_adjust_arg_t *)data;
+	zone_node_t *node = *tnode;
+
+	// remember first node
+	if (args->first_node == NULL) {
+		args->first_node = node;
+	}
+
+	// set pointer to previous node
+	if (args->adjust_prevs) {
+		node->prev = args->previous_node;
+	}
+
+	// update remembered previous pointer only if authoritative
+	if (!(node->flags & NODE_FLAGS_NONAUTH) && node->rrset_count > 0) {
+		args->previous_node = node;
+	}
+
+	node_size(node, &args->zone_size);
+	node_max_ttl(node, &args->zone_max_ttl);
+
+	return args->adjust_cb(node, args->zone);
+}
+
+static int zone_adjust_tree(zone_tree_t *tree, const zone_contents_t *zone, adjust_cb_t adjust_cb,
+                            size_t *tree_size, uint32_t *tree_max_ttl, bool adjust_prevs)
+{
+	if (zone_tree_is_empty(tree)) {
+		return KNOT_EOK;
+	}
+
+	zone_adjust_arg_t arg = {
+		.zone = zone,
+		.adjust_cb = adjust_cb,
+		.adjust_prevs = adjust_prevs
+	};
+
+	int ret = zone_tree_apply(tree, adjust_single, &arg);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	if (adjust_prevs && arg.first_node != NULL) {
+		arg.first_node->prev = arg.previous_node;
+	}
+
+	if (tree_size != NULL) {
+		*tree_size = arg.zone_size;
+	}
+	if (tree_max_ttl != NULL) {
+		*tree_max_ttl = arg.zone_max_ttl;
+	}
+	return KNOT_EOK;
+}
+
+static int load_nsec3param(zone_contents_t *contents)
+{
+	assert(contents);
+	assert(contents->apex);
+
+	const knot_rdataset_t *rrs = NULL;
+	rrs = node_rdataset(contents->apex, KNOT_RRTYPE_NSEC3PARAM);
+	if (rrs == NULL) {
+		dnssec_nsec3_params_free(&contents->nsec3_params);
+		return KNOT_EOK;
+	}
+
+	if (rrs->count < 1) {
+		return KNOT_EINVAL;
+	}
+
+	dnssec_binary_t rdata = {
+		.size = rrs->rdata->len,
+		.data = rrs->rdata->data,
+	};
+
+	dnssec_nsec3_params_t new_params = { 0 };
+	int r = dnssec_nsec3_params_from_rdata(&new_params, &rdata);
+	if (r != DNSSEC_EOK) {
+		return KNOT_EMALF;
+	}
+
+	dnssec_nsec3_params_free(&contents->nsec3_params);
+	contents->nsec3_params = new_params;
+	return KNOT_EOK;
+}
+
+int zone_adjust_contents(zone_contents_t *zone, adjust_cb_t nodes_cb, adjust_cb_t nsec3_cb)
+{
+	int ret = load_nsec3param(zone);
+	if (ret != KNOT_EOK) {
+		log_zone_error(zone->apex->owner,
+		               "failed to load NSEC3 parameters (%s)",
+		               knot_strerror(ret));
+		return ret;
+	}
+	zone->dnssec = node_rrtype_is_signed(zone->apex, KNOT_RRTYPE_SOA);
+
+	size_t nodes_size = 0, nsec3_size = 0;
+	uint32_t nodes_max_ttl = 0, nsec3_max_ttl = 0;
+
+	if (nsec3_cb != NULL) {
+		ret = zone_adjust_tree(zone->nsec3_nodes, zone, nsec3_cb, &nsec3_size, &nsec3_max_ttl, true);
+	}
+	if (ret == KNOT_EOK && nodes_cb != NULL) {
+		ret = zone_adjust_tree(zone->nodes, zone, nodes_cb, &nodes_size, &nodes_max_ttl, true);
+	}
+	if (ret == KNOT_EOK && nodes_cb != NULL && nsec3_cb != NULL) {
+		zone->size = nodes_size + nsec3_size;
+		zone->max_ttl = MAX(nodes_max_ttl, nsec3_max_ttl);
+	}
+	return ret;
+}
+
+int zone_adjust_update(zone_update_t *update, adjust_cb_t nodes_cb, adjust_cb_t nsec3_cb)
+{
+	int ret = KNOT_EOK;
+	if (nsec3_cb != NULL) {
+		ret = zone_adjust_tree(update->a_ctx->nsec3_ptrs, update->new_cont, nsec3_cb, NULL, NULL, false);
+	}
+	if (ret == KNOT_EOK && nodes_cb != NULL) {
+		ret = zone_adjust_tree(update->a_ctx->node_ptrs, update->new_cont, nodes_cb, NULL, NULL, false);
+	}
+	return ret;
+}
+
+int zone_adjust_full(zone_contents_t *zone)
+{
+	int ret = zone_adjust_contents(zone, adjust_cb_flags, adjust_cb_nsec3_flags);
+	if (ret == KNOT_EOK) {
+		ret = zone_adjust_contents(zone, adjust_cb_nsec3_and_additionals, NULL);
+	}
+	return ret;
+}
diff --git a/src/knot/zone/adjust.h b/src/knot/zone/adjust.h
new file mode 100644
index 0000000000000000000000000000000000000000..5469bed301b575eca19a906fa95deb7c44676361
--- /dev/null
+++ b/src/knot/zone/adjust.h
@@ -0,0 +1,96 @@
+/*  Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/zone/contents.h"
+#include "knot/updates/zone-update.h"
+
+typedef int (*adjust_cb_t)(zone_node_t *, const zone_contents_t *);
+
+/*
+ * \brief Varoius callbacks for adjusting zone node's params and pointers.
+ *
+ * \param node   Node to be adjusted. Must be already inside the zone contents!
+ * \param zone   Zone being adjusted.
+ *
+ * \return KNOT_E*
+ */
+
+// fix NORMAL node flags, like NODE_FLAGS_NONAUTH, NODE_FLAGS_DELEG etc.
+int adjust_cb_flags(zone_node_t *node, const zone_contents_t *zone);
+
+// fix NORMAL node pointer to corresponding NSEC3 node
+int adjust_cb_point_to_nsec3(zone_node_t *node, const zone_contents_t *zone);
+
+// fix NORMAL node pointer to NSEC3 node proving nonexistence of wildcard
+int adjust_cb_wildcard_nsec3(zone_node_t *node, const zone_contents_t *zone);
+
+// fix NSEC3 node flags: NODE_FLAGS_IN_NSEC3_CHAIN
+int adjust_cb_nsec3_flags(zone_node_t *node, const zone_contents_t *zone);
+
+// fix NORMAL node flags to additionals, like NS records and glue...
+int adjust_cb_additionals(zone_node_t *node, const zone_contents_t *zone);
+
+// adjust_cb_flags and adjust_cb_additionals at once
+int adjust_cb_flags_and_additionals(zone_node_t *node, const zone_contents_t *zone);
+
+// adjust_cb_flags and adjust_cb_flags at once
+int adjust_cb_flags_and_nsec3(zone_node_t *node, const zone_contents_t *zone);
+
+// adjust_cb_point_to_nsec3, adjust_cb_wildcard_nsec3 and adjust_cb_additionals at once
+int adjust_cb_nsec3_and_additionals(zone_node_t *node, const zone_contents_t *zone);
+
+// dummy callback, just make prev pointers adjusting and zone size measuring work
+int adjust_cb_void(zone_node_t *node, const zone_contents_t *zone);
+
+/*!
+ * \brief Apply callback to NSEC3 and NORMAL nodes. Fix PREV pointers and measure zone size.
+ *
+ * \param zone       Zone to be adjusted.
+ * \param nodes_cb   Callback for NORMAL nodes.
+ * \param nsec3_cb   Callback for NSEC3 nodes.
+ *
+ * \return KNOT_E*
+ */
+int zone_adjust_contents(zone_contents_t *zone, adjust_cb_t nodes_cb, adjust_cb_t nsec3_cb);
+
+/*!
+ * \brief Apply callback to nodes affected by the zone update.
+ *
+ * \note Fixing PREV pointers and zone measurement does not make sense since we are not
+ *       iterating over whole zone. The same applies for callback that reference other
+ *       (unchanged, but indirecty affected) zone nodes.
+ *
+ * \param update     Zone update being finalized.
+ * \param nodes_cb   Callback for NORMAL nodes.
+ * \param nsec3_cb   Callback for NSEC3 nodes.
+ *
+ * \return KNOT_E*
+ */
+int zone_adjust_update(zone_update_t *update, adjust_cb_t nodes_cb, adjust_cb_t nsec3_cb);
+
+/*!
+ * \brief Do a general-purpose full update.
+ *
+ * This operates in two phases, first fix basic node flags and prev pointers,
+ * than nsec3-related pointers and additionals.
+ *
+ * \param zone   Zone to be adjusted.
+ *
+ * \return KNOT_E*
+ */
+int zone_adjust_full(zone_contents_t *zone);
diff --git a/src/knot/zone/contents.c b/src/knot/zone/contents.c
index 2446f2e524b9f549483f18ab8ccaeb18887a2406..eef1d113756b29b535acc6422c210bef2868aa71 100644
--- a/src/knot/zone/contents.c
+++ b/src/knot/zone/contents.c
@@ -17,6 +17,7 @@
 #include <assert.h>
 
 #include "libdnssec/error.h"
+#include "knot/zone/adjust.h"
 #include "knot/zone/contents.h"
 #include "knot/common/log.h"
 #include "knot/dnssec/zone-nsec.h"
@@ -29,12 +30,6 @@ typedef struct {
 	void *data;
 } zone_tree_func_t;
 
-typedef struct {
-	zone_node_t *first_node;
-	zone_contents_t *zone;
-	zone_node_t *previous_node;
-} zone_adjust_arg_t;
-
 static int tree_apply_cb(zone_node_t **node, void *data)
 {
 	if (node == NULL || data == NULL) {
@@ -93,311 +88,15 @@ static int destroy_node_rrsets_from_tree(zone_node_t **node, void *data)
 	return KNOT_EOK;
 }
 
-static int create_nsec3_name(uint8_t *out, size_t out_size,
-                             const zone_contents_t *zone,
-                             const knot_dname_t *name)
-{
-	assert(out);
-	assert(zone);
-	assert(name);
-
-	if (!knot_is_nsec3_enabled(zone)) {
-		return KNOT_ENSEC3PAR;
-	}
-
-	return knot_create_nsec3_owner(out, out_size, name, zone->apex->owner,
-	                               &zone->nsec3_params);
-}
-
-/*! \brief Link pointers to additional nodes for this RRSet. */
-static int discover_additionals(const knot_dname_t *owner, struct rr_data *rr_data,
-                                zone_contents_t *zone)
-{
-	assert(rr_data != NULL);
-
-	/* Drop possible previous additional nodes. */
-	additional_clear(rr_data->additional);
-	rr_data->additional = NULL;
-
-	const knot_rdataset_t *rrs = &rr_data->rrs;
-	uint16_t rdcount = rrs->count;
-
-	uint16_t mandatory_count = 0;
-	uint16_t others_count = 0;
-	glue_t mandatory[rdcount];
-	glue_t others[rdcount];
-
-	/* Scan new additional nodes. */
-	for (uint16_t i = 0; i < rdcount; i++) {
-		knot_rdata_t *rdata = knot_rdataset_at(rrs, i);
-		const knot_dname_t *dname = knot_rdata_name(rdata, rr_data->type);
-		const zone_node_t *node = NULL, *encloser = NULL, *prev = NULL;
-
-		/* Try to find node for the dname in the RDATA. */
-		zone_contents_find_dname(zone, dname, &node, &encloser, &prev);
-		if (node == NULL && encloser != NULL
-		    && (encloser->flags & NODE_FLAGS_WILDCARD_CHILD)) {
-			/* Find wildcard child in the zone. */
-			node = zone_contents_find_wildcard_child(zone, encloser);
-			assert(node != NULL);
-		}
-
-		if (node == NULL) {
-			continue;
-		}
-
-		glue_t *glue;
-		if ((node->flags & (NODE_FLAGS_DELEG | NODE_FLAGS_NONAUTH)) &&
-		    rr_data->type == KNOT_RRTYPE_NS &&
-		    knot_dname_in_bailiwick(node->owner, owner) >= 0) {
-			glue = &mandatory[mandatory_count++];
-			glue->optional = false;
-		} else {
-			glue = &others[others_count++];
-			glue->optional = true;
-		}
-		glue->node = node;
-		glue->ns_pos = i;
-	}
-
-	/* Store sorted additionals by the type, mandatory first. */
-	size_t total_count = mandatory_count + others_count;
-	if (total_count > 0) {
-		rr_data->additional = malloc(sizeof(additional_t));
-		if (rr_data->additional == NULL) {
-			return KNOT_ENOMEM;
-		}
-		rr_data->additional->count = total_count;
-
-		size_t size = total_count * sizeof(glue_t);
-		rr_data->additional->glues = malloc(size);
-		if (rr_data->additional->glues == NULL) {
-			free(rr_data->additional);
-			return KNOT_ENOMEM;
-		}
-
-		size_t mandatory_size = mandatory_count * sizeof(glue_t);
-		memcpy(rr_data->additional->glues, mandatory, mandatory_size);
-		memcpy(rr_data->additional->glues + mandatory_count, others,
-		       size - mandatory_size);
-	}
-
-	return KNOT_EOK;
-}
-
-static int adjust_pointers(zone_node_t **tnode, void *data)
-{
-	assert(tnode != NULL);
-	assert(data != NULL);
-
-	zone_adjust_arg_t *args = (zone_adjust_arg_t *)data;
-	zone_node_t *node = *tnode;
-
-	// remember first node
-	if (args->first_node == NULL) {
-		args->first_node = node;
-	}
-
-	// clear Removed NSEC flag so that no relicts remain
-	node->flags &= ~NODE_FLAGS_REMOVED_NSEC;
-
-	// check if this node is not a wildcard child of its parent
-	if (knot_dname_is_wildcard(node->owner)) {
-		assert(node->parent != NULL);
-		node->parent->flags |= NODE_FLAGS_WILDCARD_CHILD;
-	}
-
-	// set flags (delegation point, non-authoritative)
-	if (node->parent &&
-	    (node->parent->flags & NODE_FLAGS_DELEG ||
-	     node->parent->flags & NODE_FLAGS_NONAUTH)) {
-		node->flags |= NODE_FLAGS_NONAUTH;
-	} else if (node_rrtype_exists(node, KNOT_RRTYPE_NS) && node != args->zone->apex) {
-		node->flags |= NODE_FLAGS_DELEG;
-	} else {
-		// Default.
-		node->flags = NODE_FLAGS_AUTH;
-	}
-
-	// set pointer to previous node
-	node->prev = args->previous_node;
-
-	// update remembered previous pointer only if authoritative
-	if (!(node->flags & NODE_FLAGS_NONAUTH) && node->rrset_count > 0) {
-		args->previous_node = node;
-	}
-
-	return KNOT_EOK;
-}
-
-static int adjust_nsec3_pointers(zone_node_t **tnode, void *data)
-{
-	assert(data != NULL);
-	assert(tnode != NULL);
-
-	zone_adjust_arg_t *args = (zone_adjust_arg_t *)data;
-	zone_node_t *node = *tnode;
-	const zone_node_t *ignored;
-
-	// Connect to NSEC3 node (only if NSEC3 tree is not empty)
-	node->nsec3_wildcard_prev = NULL;
-	uint8_t nsec3_name[KNOT_DNAME_MAXLEN];
-	int ret = create_nsec3_name(nsec3_name, sizeof(nsec3_name), args->zone,
-	                            node->owner);
-	if (ret == KNOT_EOK) {
-		node->nsec3_node = zone_tree_get(args->zone->nsec3_nodes, nsec3_name);
-
-		// Connect to NSEC3 node proving nonexistence of wildcard.
-		size_t wildcard_size = knot_dname_size(node->owner) + 2;
-		if (wildcard_size <= KNOT_DNAME_MAXLEN) {
-			assert(wildcard_size > 2);
-			knot_dname_t wildcard[wildcard_size];
-			memcpy(wildcard, "\x01""*", 2);
-			memcpy(wildcard + 2, node->owner, wildcard_size - 2);
-			ret = zone_contents_find_nsec3_for_name(args->zone, wildcard, &ignored,
-			                                        (const zone_node_t **)&node->nsec3_wildcard_prev);
-			if (ret == ZONE_NAME_FOUND) {
-				node->nsec3_wildcard_prev = NULL;
-				ret = KNOT_EOK;
-			}
-		}
-	} else if (ret == KNOT_ENSEC3PAR) {
-		node->nsec3_node = NULL;
-		ret = KNOT_EOK;
-	}
-
-	return ret;
-}
-
 static int measure_size(zone_node_t *node, void *data){
 
-	size_t *size = data;
-	int rrset_count = node->rrset_count;
-	for (int i = 0; i < rrset_count; i++) {
-		knot_rrset_t rrset = node_rrset_at(node, i);
-		*size += knot_rrset_size(&rrset);
-	}
+	node_size(node, data);
 	return KNOT_EOK;
 }
 
 static int measure_max_ttl(zone_node_t *node, void *data){
 
-	uint32_t *max = data;
-	int rrset_count = node->rrset_count;
-	for (int i = 0; i < rrset_count; i++) {
-		*max = MAX(*max, node->rrs[i].ttl);
-	}
-	return KNOT_EOK;
-}
-
-static bool nsec3_params_match(const knot_rdataset_t *rrs,
-                               const dnssec_nsec3_params_t *params,
-                               size_t rdata_pos)
-{
-	assert(rrs != NULL);
-	assert(params != NULL);
-
-	knot_rdata_t *rdata = knot_rdataset_at(rrs, rdata_pos);
-
-	return (knot_nsec3_alg(rdata) == params->algorithm
-	        && knot_nsec3_iters(rdata) == params->iterations
-	        && knot_nsec3_salt_len(rdata) == params->salt.size
-	        && memcmp(knot_nsec3_salt(rdata), params->salt.data,
-	                  params->salt.size) == 0);
-}
-
-/*!
- * \brief Adjust normal (non NSEC3) node.
- *
- * Set:
- * - pointer to wildcard childs in parent nodes if applicable
- * - flags (delegation point, non-authoritative)
- * - pointer to previous node
- * - parent pointers
- *
- * \param tnode  Zone node to adjust.
- * \param data   Adjusting parameters (zone_adjust_arg_t *).
- */
-static int adjust_normal_node(zone_node_t **tnode, void *data)
-{
-	assert(tnode != NULL && *tnode);
-	assert(data != NULL);
-
-	// Do cheap operations first
-	int ret = adjust_pointers(tnode, data);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	zone_adjust_arg_t *arg = data;
-	measure_size(*tnode, &arg->zone->size);
-	measure_max_ttl(*tnode, &arg->zone->max_ttl);
-
-	// Connect nodes to their NSEC3 nodes
-	return adjust_nsec3_pointers(tnode, data);
-}
-
-/*!
- * \brief Adjust NSEC3 node.
- *
- * Set:
- * - pointer to previous node
- * - pointer to node stored in owner dname
- *
- * \param tnode  Zone node to adjust.
- * \param data   Adjusting parameters (zone_adjust_arg_t *).
- */
-static int adjust_nsec3_node(zone_node_t **tnode, void *data)
-{
-	assert(data != NULL);
-	assert(tnode != NULL);
-
-	zone_adjust_arg_t *args = (zone_adjust_arg_t *)data;
-	zone_node_t *node = *tnode;
-
-	// remember first node
-	if (args->first_node == NULL) {
-		args->first_node = node;
-	}
-
-	// set previous node
-	node->prev = args->previous_node;
-	args->previous_node = node;
-
-	measure_size(*tnode, &args->zone->size);
-	measure_max_ttl(*tnode, &args->zone->max_ttl);
-
-	// check if this node belongs to correct chain
-	const knot_rdataset_t *nsec3_rrs = node_rdataset(node, KNOT_RRTYPE_NSEC3);
-	for (uint16_t i = 0; nsec3_rrs != NULL && i < nsec3_rrs->count; i++) {
-		if (nsec3_params_match(nsec3_rrs, &args->zone->nsec3_params, i)) {
-			node->flags |= NODE_FLAGS_IN_NSEC3_CHAIN;
-		}
-	}
-
-	return KNOT_EOK;
-}
-
-/*! \brief Discover additional records for affected nodes. */
-static int adjust_additional(zone_node_t **tnode, void *data)
-{
-	assert(data != NULL);
-	assert(tnode != NULL);
-
-	zone_adjust_arg_t *args = (zone_adjust_arg_t *)data;
-	zone_node_t *node = *tnode;
-
-	/* Lookup additional records for specific nodes. */
-	for(uint16_t i = 0; i < node->rrset_count; ++i) {
-		struct rr_data *rr_data = &node->rrs[i];
-		if (knot_rrtype_additional_needed(rr_data->type)) {
-			int ret = discover_additionals(node->owner, rr_data, args->zone);
-			if (ret != KNOT_EOK) {
-				return ret;
-			}
-		}
-	}
-
+	node_max_ttl(node, data);
 	return KNOT_EOK;
 }
 
@@ -827,7 +526,7 @@ int zone_contents_find_dname(const zone_contents_t *zone,
                              const zone_node_t **closest,
                              const zone_node_t **previous)
 {
-	if (!zone || !name || !match || !closest || !previous) {
+	if (!zone || !name || !match || !closest) {
 		return KNOT_EINVAL;
 	}
 
@@ -842,7 +541,7 @@ int zone_contents_find_dname(const zone_contents_t *zone,
 	if (found < 0) {
 		// error
 		return found;
-	} else if (found == 1) {
+	} else if (found == 1 && previous != NULL) {
 		// exact match
 
 		assert(node && prev);
@@ -851,6 +550,14 @@ int zone_contents_find_dname(const zone_contents_t *zone,
 		*closest = node;
 		*previous = prev;
 
+		return ZONE_NAME_FOUND;
+	} else if (found == 1 && previous == NULL) {
+		// exact match, zone not adjusted yet
+
+		assert(node);
+		*match = node;
+		*closest = node;
+
 		return ZONE_NAME_FOUND;
 	} else {
 		// closest match
@@ -866,7 +573,9 @@ int zone_contents_find_dname(const zone_contents_t *zone,
 
 		*match = NULL;
 		*closest = node;
-		*previous = prev;
+		if (previous != NULL) {
+			*previous = prev;
+		}
 
 		return ZONE_NAME_NOT_FOUND;
 	}
@@ -896,9 +605,13 @@ int zone_contents_find_nsec3_for_name(const zone_contents_t *zone,
 	if (zone_tree_is_empty(zone->nsec3_nodes)) {
 		return KNOT_ENSEC3CHAIN;
 	}
+	if (!knot_is_nsec3_enabled(zone)) {
+		return KNOT_ENSEC3PAR;
+	}
 
 	uint8_t nsec3_name[KNOT_DNAME_MAXLEN];
-	int ret = create_nsec3_name(nsec3_name, sizeof(nsec3_name), zone, name);
+	int ret = knot_create_nsec3_owner(nsec3_name, sizeof(nsec3_name),
+	                                  name, zone->apex->owner, &zone->nsec3_params);
 	if (ret != KNOT_EOK) {
 		return ret;
 	}
@@ -946,107 +659,6 @@ const zone_node_t *zone_contents_find_wildcard_child(const zone_contents_t *cont
 	return zone_contents_find_node(contents, wildcard);
 }
 
-static int adjust_nodes(zone_tree_t *nodes, zone_adjust_arg_t *adjust_arg,
-                        zone_tree_apply_cb_t callback)
-{
-	assert(adjust_arg);
-	assert(callback);
-
-	if (zone_tree_is_empty(nodes)) {
-		return KNOT_EOK;
-	}
-
-	adjust_arg->first_node = NULL;
-	adjust_arg->previous_node = NULL;
-
-	int ret = zone_tree_apply(nodes, callback, adjust_arg);
-
-	if (adjust_arg->first_node) {
-		adjust_arg->first_node->prev = adjust_arg->previous_node;
-	}
-
-	return ret;
-}
-
-static int load_nsec3param(zone_contents_t *contents)
-{
-	assert(contents);
-	assert(contents->apex);
-
-	const knot_rdataset_t *rrs = NULL;
-	rrs = node_rdataset(contents->apex, KNOT_RRTYPE_NSEC3PARAM);
-	if (rrs == NULL) {
-		dnssec_nsec3_params_free(&contents->nsec3_params);
-		return KNOT_EOK;
-	}
-
-	if (rrs->count < 1) {
-		return KNOT_EINVAL;
-	}
-
-	dnssec_binary_t rdata = {
-		.size = rrs->rdata->len,
-		.data = rrs->rdata->data,
-	};
-
-	dnssec_nsec3_params_t new_params = { 0 };
-	int r = dnssec_nsec3_params_from_rdata(&new_params, &rdata);
-	if (r != DNSSEC_EOK) {
-		return KNOT_EMALF;
-	}
-
-	dnssec_nsec3_params_free(&contents->nsec3_params);
-	contents->nsec3_params = new_params;
-	return KNOT_EOK;
-}
-
-static int contents_adjust(zone_contents_t *contents, bool normal)
-{
-	if (contents == NULL || contents->apex == NULL) {
-		return KNOT_EINVAL;
-	}
-
-	int ret = load_nsec3param(contents);
-	if (ret != KNOT_EOK) {
-		log_zone_error(contents->apex->owner,
-		               "failed to load NSEC3 parameters (%s)",
-		               knot_strerror(ret));
-		return ret;
-	}
-
-	zone_adjust_arg_t arg = {
-		.zone = contents
-	};
-
-	contents->size = 0;
-	contents->dnssec = node_rrtype_is_signed(contents->apex, KNOT_RRTYPE_SOA);
-
-	// NSEC3 nodes must be adjusted first, because we already need the NSEC3 chain
-	// to be closed before we adjust NSEC3 pointers in adjust_normal_node
-	ret = adjust_nodes(contents->nsec3_nodes, &arg, adjust_nsec3_node);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	ret = adjust_nodes(contents->nodes, &arg,
-	                   normal ? adjust_normal_node : adjust_pointers);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	return adjust_nodes(contents->nodes, &arg, adjust_additional);
-}
-
-int zone_contents_adjust_pointers(zone_contents_t *contents)
-{
-	return contents_adjust(contents, false);
-}
-
-int zone_contents_adjust_full(zone_contents_t *contents)
-{
-	return contents_adjust(contents, true);
-}
-
 int zone_contents_apply(zone_contents_t *contents,
                         zone_contents_apply_cb_t function, void *data)
 {
diff --git a/src/knot/zone/contents.h b/src/knot/zone/contents.h
index e8ffa96103675025d39f4534771dac1cd8e80e27..22c25951ca2938fdede76fb3ca476cdc92a786d3 100644
--- a/src/knot/zone/contents.h
+++ b/src/knot/zone/contents.h
@@ -173,22 +173,6 @@ int zone_contents_find_nsec3_for_name(const zone_contents_t *contents,
 const zone_node_t *zone_contents_find_wildcard_child(const zone_contents_t *contents,
                                                      const zone_node_t *parent);
 
-/*!
- * \brief Sets parent and previous pointers and node flags. (cheap operation)
- *        For both normal and NSEC3 tree
- *
- * \param contents Zone contents to be adjusted.
- */
-int zone_contents_adjust_pointers(zone_contents_t *contents);
-
-/*!
- * \brief Sets parent and previous pointers, sets node flags and NSEC3 links.
- *        This has to be called before the zone can be served.
- *
- * \param contents Zone contents to be adjusted.
- */
-int zone_contents_adjust_full(zone_contents_t *contents);
-
 /*!
  * \brief Applies the given function to each regular node in the zone.
  *
diff --git a/src/knot/zone/node.c b/src/knot/zone/node.c
index 4d920717d4b1072985b8ec9673ec8e8ab51efdb9..117d3c81a5651a6b729758eb87f42a16f81a7428 100644
--- a/src/knot/zone/node.c
+++ b/src/knot/zone/node.c
@@ -16,6 +16,7 @@
 
 #include "knot/zone/node.h"
 #include "libknot/libknot.h"
+#include "contrib/macros.h"
 #include "contrib/mempattern.h"
 
 void additional_clear(additional_t *additional)
@@ -310,3 +311,20 @@ bool node_bitmap_equal(const zone_node_t *a, const zone_node_t *b)
 	}
 	return true;
 }
+
+void node_size(const zone_node_t *node, size_t *size)
+{
+	int rrset_count = node->rrset_count;
+	for (int i = 0; i < rrset_count; i++) {
+		knot_rrset_t rrset = node_rrset_at(node, i);
+		*size += knot_rrset_size(&rrset);
+	}
+}
+
+void node_max_ttl(const zone_node_t *node, uint32_t *max)
+{
+	int rrset_count = node->rrset_count;
+	for (int i = 0; i < rrset_count; i++) {
+		*max = MAX(*max, node->rrs[i].ttl);
+	}
+}
diff --git a/src/knot/zone/node.h b/src/knot/zone/node.h
index b380e8fefebcc14b250335b7d5884ca1963b64b0..198785f899ab417baba5c772bd6e4f0ab1808c92 100644
--- a/src/knot/zone/node.h
+++ b/src/knot/zone/node.h
@@ -276,3 +276,19 @@ static inline knot_rrset_t node_rrset_at(const zone_node_t *node, size_t pos)
 	rrset.additional = rr_data->additional;
 	return rrset;
 }
+
+/*!
+ * \brief Compute node size.
+ *
+ * \param node   Node in question.
+ * \param size   In/out: node size will be added to this value.
+ */
+void node_size(const zone_node_t *node, size_t *size);
+
+/*!
+ * \brief Compute node maximum TTL.
+ *
+ * \param node   Node in question.
+ * \param size   In/out: this value will be maximalized with max TTL of node rrsets.
+ */
+void node_max_ttl(const zone_node_t *node, uint32_t *max);
diff --git a/src/knot/zone/zone-load.c b/src/knot/zone/zone-load.c
index ff241fae3fa5663b6b00686b169e9fc75e6e888c..ccbb738ac6bdd5446347234bb61b4862d2db3797 100644
--- a/src/knot/zone/zone-load.c
+++ b/src/knot/zone/zone-load.c
@@ -85,7 +85,11 @@ int zone_load_journal(conf_t *conf, zone_t *zone, zone_contents_t *contents)
 
 	/* Apply changesets. */
 	apply_ctx_t a_ctx = { 0 };
-	apply_init_ctx(&a_ctx, contents, 0);
+	ret = apply_init_ctx(&a_ctx, contents, 0);
+	if (ret != KNOT_EOK) {
+		changesets_free(&chgs);
+		return ret;
+	}
 
 	ret = apply_changesets_directly(&a_ctx, &chgs);
 	if (ret == KNOT_EOK) {
@@ -126,7 +130,12 @@ int zone_load_from_journal(conf_t *conf, zone_t *zone, zone_contents_t **content
 	}
 
 	apply_ctx_t a_ctx = { 0 };
-	apply_init_ctx(&a_ctx, *contents, 0);
+	ret = apply_init_ctx(&a_ctx, *contents, 0);
+	if (ret != KNOT_EOK) {
+		changesets_free(&chgs);
+		return ret;
+	}
+
 	ret = apply_changesets_directly(&a_ctx, &chgs);
 	if (ret == KNOT_EOK) {
 		log_zone_info(zone->name, "zone loaded from journal, serial %u",
diff --git a/src/knot/zone/zone-tree.c b/src/knot/zone/zone-tree.c
index 443682f17cf1356b30dc59e496fa17d1e4ab7a8d..b638dd18295a941c1ee357e760070ef3b817a5c7 100644
--- a/src/knot/zone/zone-tree.c
+++ b/src/knot/zone/zone-tree.c
@@ -114,11 +114,9 @@ int zone_tree_get_less_or_equal(zone_tree_t *tree,
 }
 
 /*! \brief Removes node with the given owner from the zone tree. */
-static void remove_node(zone_tree_t *tree, const knot_dname_t *owner)
+void zone_tree_remove_node(zone_tree_t *tree, const knot_dname_t *owner)
 {
-	assert(owner);
-
-	if (zone_tree_is_empty(tree)) {
+	if (zone_tree_is_empty(tree) || owner == NULL) {
 		return;
 	}
 
@@ -159,7 +157,7 @@ void zone_tree_delete_empty(zone_tree_t *tree, zone_node_t *node)
 		}
 
 		// Delete node
-		remove_node(tree, node->owner);
+		zone_tree_remove_node(tree, node->owner);
 		node_free(node, NULL);
 	}
 }
diff --git a/src/knot/zone/zone-tree.h b/src/knot/zone/zone-tree.h
index baac735bf34c571ce147bab7f276788493f3e546..e31ae2c22df4439304eb957c91c4d550de586252 100644
--- a/src/knot/zone/zone-tree.h
+++ b/src/knot/zone/zone-tree.h
@@ -107,6 +107,14 @@ int zone_tree_get_less_or_equal(zone_tree_t *tree,
                                 zone_node_t **found,
                                 zone_node_t **previous);
 
+/*!
+ * \brief Remove a node from a tree with no checks.
+ *
+ * \param tree  The tree to remove from.
+ * \param owner The node to remove.
+ */
+void zone_tree_remove_node(zone_tree_t *tree, const knot_dname_t *owner);
+
 /*!
  * \brief Delete a node that has no RRSets and no children.
  *
diff --git a/src/knot/zone/zonefile.c b/src/knot/zone/zonefile.c
index dcbfbaa9c2b39315842d8906dd4ba53d468ef5c6..8caa0ab36c9daab54c512a0c5702f3e341658abe 100644
--- a/src/knot/zone/zonefile.c
+++ b/src/knot/zone/zonefile.c
@@ -30,6 +30,7 @@
 #include "knot/common/log.h"
 #include "knot/dnssec/zone-nsec.h"
 #include "knot/zone/semantic-check.h"
+#include "knot/zone/adjust.h"
 #include "knot/zone/contents.h"
 #include "knot/zone/zonefile.h"
 #include "knot/zone/zone-dump.h"
@@ -225,7 +226,7 @@ zone_contents_t *zonefile_load(zloader_t *loader)
 		goto fail;
 	}
 
-	ret = zone_contents_adjust_full(zc->z);
+	ret = zone_adjust_contents(zc->z, adjust_cb_flags_and_nsec3, adjust_cb_nsec3_flags);
 	if (ret != KNOT_EOK) {
 		ERROR(zname, "failed to finalize zone contents (%s)",
 		      knot_strerror(ret));
diff --git a/tests/knot/test_server.h b/tests/knot/test_server.h
index 64d69b023bee071e3d047ad7e956af924a327a0b..8107215645c3735a51b023945695fc1b22003c32 100644
--- a/tests/knot/test_server.h
+++ b/tests/knot/test_server.h
@@ -18,6 +18,7 @@
 
 #include "test_conf.h"
 #include "knot/server/server.h"
+#include "knot/zone/adjust.h"
 #include "contrib/mempattern.h"
 
 /* Some domain names. */
@@ -52,7 +53,7 @@ static inline void create_root_zone(server_t *server, knot_mm_t *mm)
 	knot_rrset_free(soa, mm);
 
 	/* Bake the zone. */
-	zone_contents_adjust_full(root->contents);
+	(void)zone_adjust_full(root->contents);
 
 	/* Switch zone db. */
 	knot_zonedb_free(&server->zone_db);