diff --git a/examples/zzEstimatorWithScottZhang.m b/examples/zzEstimatorWithScottZhang.m
new file mode 100644
index 0000000000000000000000000000000000000000..861cd38620697d82534a1273e1c10b63d5935fc9
--- /dev/null
+++ b/examples/zzEstimatorWithScottZhang.m
@@ -0,0 +1,136 @@
+% ******************************************************************************
+% Prototypical AFEM example with iterative solver for higher order finite
+% elements steered by ZZ-estimator with Scott-Zhang as averaging operator.
+% ******************************************************************************
+
+%% parameters
+nDofsMax = 1e5;
+theta = 0.5;
+pmax = 4;
+lamalg = 0.1;
+
+%% initialization for every polynomial degree
+[nElem, nDofs, errEst, iterations, time, directSolveTime] = deal(zeros(pmax, 1000));
+for p = 1:pmax
+ %% setup geometry & spaces
+ printLogMessage('*** p = %d (of %d) ***', p, pmax)
+ mesh = Mesh.loadFromGeometry('Lshape');
+ fes = FeSpace(mesh, HigherOrderH1Fe(p), 'dirichlet', ':');
+ u = FeFunction(fes);
+ u.setData(0);
+
+ %% set problem data for -\Delta u = 1 on L-shape
+ blf = BilinearForm();
+ lf = LinearForm();
+
+ blf.a = Constant(mesh, 1);
+ lf.f = Constant(mesh, 1);
+
+ %% set up solver and operator for nested iteration
+ % choose the iterative solver of choice: multigrid (with the variants
+ % lowOrderVcycle and highOrderVcycle), pcg (with jacobi and additive
+ % Schwarz preconditioner), and the cg solver
+ %[solver, P] = IterativeSolver.choose(fes, blf, "pcg", "iChol");
+ %[solver, P] = IterativeSolver.choose(fes, blf, "pcg", "additiveSchwarzHighOrder");
+ [solver, P] = IterativeSolver.choose(fes, blf, "multigrid", "lowOrderVcycle");
+ solver.tol = 1e-6;
+ solver.maxIter = 100;
+
+ %% adaptive loop
+ ell = 0;
+ meshSufficientlyFine = false;
+ while ~meshSufficientlyFine
+ %% setup
+ tic;
+ ell = ell + 1;
+ A = assemble(blf, fes);
+ rhs = assemble(lf, fes);
+
+ freeDofs = getFreeDofs(fes);
+ A = A(freeDofs,freeDofs);
+ solver.setupSystemMatrix(A);
+ solver.setupRhs(rhs(freeDofs), u.data(freeDofs)');
+
+ % exact solution as reference
+ tic;
+ x_ex = A \ rhs(freeDofs);
+ directSolveTime(p, ell) = toc;
+
+ %% iterative solve
+ % solver.solve() implements the algebraic solver loop and
+ % terminates automatically once the wanted accuracy is reached
+ % Another option is to manually implement a loop and implement one
+ % solver step with solver.step() and stop by setting a stopping
+ % criterion
+ solverIsConverged = false;
+ while ~solverIsConverged
+ solver.step();
+ u.setFreeData(solver.x)
+ mu2 = estimateZZ(blf, lf, u);
+ estimator = sqrt(sum(mu2));
+ solver.tol = lamalg*estimator;
+ solverIsConverged = solver.isConverged;
+ end
+
+
+ %% estimate error and store data
+ nDofs(p,ell) = getDofs(fes).nDofs;
+ errEst(p,ell) = estimator;
+ iterations(p, ell) = solver.iterationCount();
+ nElem(p,ell) = mesh.nElements;
+ printLogMessage('number of dofs: %d, estimator: %.2e after %d iterations', ...
+ nDofs(p,ell), errEst(p,ell), solver.iterationCount);
+
+ %% stoping criterion
+ meshSufficientlyFine = (nDofs(p,ell) > nDofsMax);
+
+ %% refine mesh
+ if ~meshSufficientlyFine
+ marked = markDoerflerSorting(mu2, theta);
+ mesh.refineLocally(marked, 'NVB');
+ u.setData(prolongate(P, u));
+ end
+ time(p,ell) = toc;
+ end
+end
+
+figure()
+for p = 1:pmax
+ idx = find(nDofs(p,:) > 0);
+ complexity = [cumsum(nDofs(p, idx) .* iterations(p, idx))];
+ loglog(complexity, errEst(p,idx), '-o', 'LineWidth', 2, 'DisplayName', ['p=',num2str(p)]);
+ hold on
+end
+for p = unique([1,pmax])
+ x = complexity / complexity(1);
+ loglog(complexity(1)*x, errEst(p,1)*x.^(-p/2), '--', 'LineWidth', 2, 'Color', 'k', 'DisplayName', ['\alpha = ', num2str(p), '/2'])
+end
+legend
+xlabel('computational cost')
+ylabel('\eta')
+title('error estimator over number of total computational cost')
+
+
+%% local function for recovery based a posteriori error estimation
+
+function indicators = estimateZZ(~, ~, u)
+
+p = u.fes.finiteElement.order;
+qr = QuadratureRule.ofOrder(2*p);
+
+% Remark: Scott-Zhang operator for piecewise polynomials of degree p corresponds to nodal interpolation
+
+% nodal interpolation of first component of gradient of u
+gGradU1 = FeFunction(u.fes);
+gGradU1.setData(nodalInterpolation(CompositeFunction(@(grad) grad(1,:,:), Gradient(u)), u.fes));
+
+% nodal interpolation of second component of gradient of u
+gGradU2 = FeFunction(u.fes);
+gGradU2.setData(nodalInterpolation(CompositeFunction(@(grad) grad(2,:,:), Gradient(u)), u.fes));
+
+% compute recovery term
+recoveryTermHandle = @(g1, g2, grad) (g1 - grad(1,:,:)).^2 + (g2 - grad(2,:,:)).^2;
+recoveryTerm = CompositeFunction(recoveryTermHandle, gGradU1, gGradU2, Gradient(u));
+indicators = integrateElement(recoveryTerm, qr);
+
+end