FreshMarker Partial Template Reduction

“There is a miracle in every new beginning.”

Hermann Hesse

Some blog posts are about ideas and not solutions. This has the pleasant advantage for the author that the content of the contribution does not (yet) have to work. It also saves the developer work if the audience does not appreciate the topic.

When working with templates, there are always a number of variables that are processed in a template with always the same value. Every evaluation of the template produces identical output for some vaiables. It would be nice if you could save computing time there.

In a tariff newsletter, there are always parts that are identical for every newsletter.

Accelerate Your Internet with Our New Fiber Optic Plan!

Hi ${customer.firstname} ${customer.lastname},
We're thrilled to offer you a personalized fiber optic plan featuring 
${product.mbps} Mbps of lightning-fast speeds for just € ${product.price} per month. 
With our fiber optic connection, you’ll experience seamless streaming, gaming, 
and browsing without any disruptions. 

Don’t miss out on this opportunity to future-proof your home with 
cutting-edge technology. 

Click the link below to get started today and join the thousands of satisfied customers 
who have already made the switch!

Best regards,

Your ${company.name} Team

It may not be the prettiest newsletter, but at least it’s generated by an AI.

This template contains five interpolations that must be evaluated for each email. The first two interpolations for the customer name ${customer.firstname} and ${customer.lastname} are most likely different in each newsletter. If different customer groups are offered different products or product prices, then ${product.mbps} and ${product.price} are identical in many or all newsletters. The last interpolation ${company.name} is identical in all newsletters. In the case of a white label system, however, several values could also be found here.

For a simple case where the newsletter is sent to 10,000 customers of a company and three different prices are offered for the same product, three interpolations are always evaluated with the same result.

To save yourself this work, the template could be adapted and the three interpolations could be replaced by there actual values. However, with 50 companies, each with 10,000 customers, nobody wants to have 50 different templates available.

One possible solution to the problem is to partially reduce the original template for each company and product and then generate all newsletters for the company’s customers using the new template.

The following is a simple example of sending a newsletter without partial reduction.

List<Product> products = productService.getCampaignedProducts(campaign);
List<Company> companies = companyRepository.findByCampaign(campaign);

Template template = config.getTemplate(Path.of("template/newletter.tpl"));
for (Company company : companies) {
  for (Product product products) {
    List<Customer> customers = customerService.findBySuggestion(company, product);
    for (Customer customer : customers) {
      Map<String, Object> model = Map.of("company", company, "customer", customer, "product", product);
      String email = template.process(model);
      sendEmail(customer, email);
    }
  }
}

For each customer of a company, the company, the customer and the proposed product are inserted into the model. The newsletter text is then generated using the template and the model and then sent as an e-mail.

With partial reduction of the template, the sample code changes only minimally. At the beginning of the product loop, a new template is created from the existing template at which the reduce method is called. This method receives all the model data that is already to be evaluated in the template.

List<Product> products = productService.getCampaignedProducts(campaign);
List<Company> companies = companyRepository.findByCampaign(campaign);

Template template = config.getTemplate(Path.of("template/newletter.tpl"));
for (Company company : companies) {
  for (Product product products) {
    Template customerTemplate = template.reduce(Map.of("company", company, "product", product));
    List<Customer> customers = customerService.findBySuggestion(company, product);  
    for (Customer customer : customers) {
      String email = customerTemplate.process(Map.of("customer", customer));
      sendEmail(customer, email);
    }
  }
}

In this example, the generated customerTemplate only needs to be filled with the customer’s data because the other data has already been replaced by constant fragments.

The actual work for partial reduction takes place in the reduce method. It runs through the tree of Fragment instances in the Template and changes and deletes instances. This is most obvious for the InterpolationFragment. This Fragment generates a text from an interpolation and the model.

With a partial model, the Interpolation Directive can be resolved if the necessary data is available in the model, or the processing throws an exception if the evaluation has insufficient data. A reduce method for the InterpolationFragment must therefore only check whether the interpolation can be evaluated. In the positive case, the Fragment is replaced by a ConstantFragment and in the negative case, the Fragment itself is retained.

Other interesting Fragment instances for partial reduction are those for the List-, Switch- and If-Directives.

With the List-Directive, the embedded Fragment instances can be partially reduced. Interpolations that contain Loop Variables are more difficult to evaluate. However, this would also require the loops to be unwound. Whether this is actually worthwhile for normal templates is questionable.

With the If- and Switch-Directives, not only the embedded Fragment instances can be partially reduced. If the conditional expression can be evaluated for all cases, the corresponding conditional directive can be replaced by the only possible case.

<#if value=1>
Eins
<#elseif value=2>
Zwei
<#else>
Viele
</#if>

In the example shown here, the If-Directive can be simplified if the variable value is set. If the value is 1, the If-Directive can be replaced by the value Eins. If the value is 2, the If-Directive can be replaced by the value Zwei. For all other values, the directive can be replaced by Viele. For an If-Directive without an Else branch, the If-Directive can be omitted completely for these values.

In addition to the Loop Variables, some other features also make the partial reduction of templates more difficult. We will soon see what hurdles the implementation has to overcome.

1 thought on “FreshMarker Partial Template Reduction”

Leave a Comment