I spent last night chasing a weird email bug. A JSP based template was adding a rogue space in the invoice total. Outlook showed 12 345.67 while Gmail showed 12345.67. I finally moved the email to FreeMarker, gave the numbers a proper format, and the ghost space vanished. Same data, cleaner template, no magic. That little win pushed me to sit down and write how I have been using FreeMarker for email and PDFs in real projects running on Tomcat and friends.
\n\n\n\nWhy FreeMarker beats JSP for templates you send to people
\n\n\n\nJSP is fine for pages you render live. For email templates and PDF sources, I want a text file that designers and copy folks can read without Java in the middle. FreeMarker gives me clear placeholders, strict syntax, and good error messages when something is missing. It does not hide nulls. It forces you to be clear. That single trait saves hours when a campaign goes out and an address field is empty. I still pass a plain Map from a Spring or Struts action, but what the template sees is tidy and predictable. Better separation, less surprise, fewer late night edits in production.
\n\n\n\nOn email, FreeMarker shines. I keep one subject.ftl for the subject, one body.html.ftl for the HTML version, and one body.txt.ftl for the text version. Same data model, two outputs. The HTML version gets basic tables because Outlook and Lotus Notes strip many CSS rules. Inline styles are kept simple. Images go inline with Content ID so no broken links when the client is offline. I keep translations in ResourceBundle and pass labels into the template. Encoding stays UTF 8 from template to message. Subjects can be templated too, which is great for order numbers or names.
\n\n\n\n// FreeMarker config\nConfiguration cfg = new Configuration(Configuration.VERSION_2_3_23);\ncfg.setClassForTemplateLoading(getClass(), "/mail");\ncfg.setDefaultEncoding("UTF-8");\ncfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);\n\n// Render subject and bodies\nTemplate subjectT = cfg.getTemplate("subject.ftl");\nTemplate htmlT = cfg.getTemplate("body.html.ftl");\nTemplate textT = cfg.getTemplate("body.txt.ftl");\n\nStringWriter out = new StringWriter();\nsubjectT.process(model, out);\nString subject = out.toString();\n\nString html = FreeMarkerTemplateUtils.processTemplateIntoString(htmlT, model);\nString text = FreeMarkerTemplateUtils.processTemplateIntoString(textT, model);\n\n// JavaMail multipart message\nMimeMessage msg = new MimeMessage(session);\nmsg.setFrom(new InternetAddress("billing@example.com"));\nmsg.setRecipients(Message.RecipientType.TO, toAddress);\nmsg.setSubject(subject, "UTF-8");\n\nMimeBodyPart textPart = new MimeBodyPart();\ntextPart.setText(text, "UTF-8");\n\nMimeBodyPart htmlPart = new MimeBodyPart();\nhtmlPart.setContent(html, "text/html; charset=UTF-8");\n\nMimeMultipart alt = new MimeMultipart("alternative");\nalt.addBodyPart(textPart);\nalt.addBodyPart(htmlPart);\nmsg.setContent(alt);\nTransport.send(msg);\n\n\n\nFor testing I like a local SMTP sink like Dumbster or a simple Wiser instance. I render both versions and snapshot them with sample data. The rule I follow is this. Plain text must be good on its own and the HTML must degrade without breaking meaning. FreeMarker makes that easy because the same Date and Number formatting can live in a macro. One more tip. keep the money format in a shared macro and never repeat it. You do not want your cents to round differently between the text and the HTML. The template engine will do what you tell it. Be precise.
\n\n\n\nFrom FreeMarker to PDF without tears
\n\n\n\nMoving from FreeMarker to PDF gives you two stable routes. You can generate XSL FO and feed it to Apache FOP. Or you can produce structured text and draw with iText. FO feels more like template land, which keeps designers closer to the source. iText feels like code that draws boxes and text. For invoices and receipts my choice lately has been FO via FreeMarker. The markup reads like print layout, page breaks are clear, and you can keep all the math in Java before you render. FOP is solid if you pick fonts carefully and embed them. That avoids the classic missing glyph in totals.
\n\n\n\n// Render FO with FreeMarker\nTemplate foT = cfg.getTemplate("invoice.fo.ftl");\nString fo = FreeMarkerTemplateUtils.processTemplateIntoString(foT, model);\n\n// Run through FOP\nFopFactory fopFactory = FopFactory.newInstance();\nFOUserAgent foUserAgent = fopFactory.newFOUserAgent();\nByteArrayOutputStream pdfOut = new ByteArrayOutputStream();\nFop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, pdfOut);\n\nTransformerFactory factory = TransformerFactory.newInstance();\nTransformer transformer = factory.newTransformer();\nSource src = new StreamSource(new StringReader(fo));\nResult res = new SAXResult(fop.getDefaultHandler());\ntransformer.transform(src, res);\n\nbyte[] pdf = pdfOut.toByteArray();\n\n\n\nIf you prefer iText, keep the template step as a simple layout map. Do all totals and wording in Java, then draw. I avoid HTML to PDF tricks for now. They look tempting but email grade HTML does not print well. A neat pattern is to let FreeMarker output a row list with positions and styles in JSON or XML, then use iText to paint it. Clear rules, no guesswork. Both paths benefit from FreeMarker handling text like names and addresses. That keeps your PDF code short and focused on geometry. Think of FreeMarker as the safe place for words, iText or FOP as the sharp tool for pages.
\n\n\n\n// iText sketch with data from model\nDocument doc = new Document(PageSize.A4, 36, 36, 36, 36);\nByteArrayOutputStream pdfBytes = new ByteArrayOutputStream();\nPdfWriter.getInstance(doc, pdfBytes);\ndoc.open();\n\nFont base = new Font(BaseFont.createFont("fonts/DejaVuSans.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED), 10);\nParagraph title = new Paragraph((String) model.get("title"), new Font(base.getBaseFont(), 14));\ndoc.add(title);\n\n// Loop order lines rendered earlier by FreeMarker to CSV or simple text\nList<Map<String, Object>> lines = (List<Map<String, Object>>) model.get("lines");\nPdfPTable table = new PdfPTable(4);\nfor (Map<String, Object> line : lines) {\n table.addCell(line.get("sku").toString());\n table.addCell(line.get("name").toString());\n table.addCell(line.get("qty").toString());\n table.addCell(line.get("total").toString());\n}\ndoc.add(table);\ndoc.close();\n\n\n\nPractical setup and a few rules to keep you sane
\n\n\n\nStore templates on the classpath for production and on the file system for local work so you get hot reload. Turn on RETHROW handler so bad variables fail fast. Define shared variables for date and number formats. Set time zone and locale on the configuration rather than in the template. Use macros for repeated chunks like headers, money, and addresses. Keep business rules out of templates. A template should read like a letter, not a calculator. On performance, cache Template objects. FreeMarker is fast when you do that. I have sent batches of fifty thousand messages per hour on a small box without trouble.
\n\n\n\nFor email deliverability, keep HTML tidy and small. Do not attach large images. Prefer inline CID for logos. Use multipart alternative with text first and HTML second. Watch character count in the subject after variables expand. Build a tiny preview screen in your admin where you can switch locales and sample orders. If you ship to many countries, keep addresses flexible. FreeMarker can handle optional fields cleanly with ?has_content and defaults like ${value!}. Log the final subject and a hash of the body to trace what went out. That makes customer support friendly and fast.
\n\n\n\nConcise summary
\n\n\n\nUse FreeMarker to keep email and PDF sources clean, testable, and safe. Let Java do math and data prep, let templates handle words and layout. For email, render both text and HTML from one model and keep styles simple for Outlook and Notes. For PDFs, generate XSL FO for FOP or draw with iText, with FreeMarker feeding the text bits. Cache templates, fail on missing data, and centralize formats. These small habits pay back fast when a campaign or invoice run goes live and you need messages and PDFs that look right every time.
\n