5 minutes. What it is, why it's structured this way, and a working example that renders a CV PDF.
Dependency. The ready-made presets ship in the opt-in
graph-compose-templatesartifact — they are not bundled ingraph-compose. Add it next tograph-compose(or depend ongraph-compose-bundle, which includes both). See the README install matrix.
GraphCompose's templates v2 (layered) gives you:
- Records describing content —
CvDocument,CvIdentity,CvSection. No styling, no rendering, just structured data. - Themes describing visuals —
BrandTheme(palette + typography + spacing + decoration). Swap a theme to change colours, fonts, bullet glyphs without touching renderers. - Widgets as visual LEGO bricks — the neutral header bricks
(
Headline,Subheadline,ContactLine) live in the sharedtemplates.core.identity; CV-specific ones (SectionHeaderand friends) incv.widgets. Each is a named visual decision you drop into a preset. - Presets as compositions — a preset orchestrates widgets in a
page flow. Sixteen ship today (
BoxedSections,ModernProfessional,NordicClean,EditorialBlue,Executive,EngineeringResume,TimelineMinimal, …); writing your own is ~150 lines.
You hand a CvDocument to a preset, you get a PDF. The preset
internally composes widgets that read theme tokens that ultimately
emit DSL calls to the engine.
import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
import com.demcha.compose.document.api.DocumentSession;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.cv.data.*;
import com.demcha.compose.document.templates.cv.presets.BoxedSections;
import java.nio.file.Path;
CvDocument doc = CvDocument.builder()
.identity(CvIdentity.builder()
.name("Jane", "Doe")
.contact("+44 20 7946 0958", "jane@example.com", "London, UK")
.link("LinkedIn", "https://linkedin.com/in/jane-doe")
.build())
.section(new ParagraphSection("Professional Summary",
"Backend engineer with **5 years** of experience building "
+ "high-throughput payment systems."))
.section(SkillsSection.builder("Technical Skills")
.group("Languages", "Java 21", "Kotlin", "SQL")
.group("Frameworks", "Spring Boot", "Quarkus")
.build())
.section(EntriesSection.builder("Experience")
.entry("Senior Engineer", "Acme Payments", "2022-Present",
"Owned the settlement service handling **2M+ tx/day**.")
.entry("Engineer", "Bright Bank", "2019-2022",
"Built the fraud-detection rule engine.")
.build())
.build();
DocumentTemplate<CvDocument> template = BoxedSections.create();
try (DocumentSession session = GraphCompose.document(Path.of("cv.pdf"))
.pageSize(DocumentPageSize.A4)
.margin(28, 28, 28, 28)
.create()) {
template.compose(session, doc);
session.buildPdf();
}Run it. You get cv.pdf rendered in the Boxed Sections visual style.
Want it in the Modern Professional style instead? Change one line:
DocumentTemplate<CvDocument> template = ModernProfessional.create();Same data, different visual. That's the layering.
┌────────────────────────────────────────────────────────────────────────────┐
│ presets/ 16 CV compositions │
│ BoxedSections, ModernProfessional, NordicClean, ... │
│ -- widgets composed in a page flow │
└────────────────────────────────────────────────────────────────────────────┘
│ compose from widgets
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ widgets/ CV-specific visual bricks │
│ SectionHeader, FlowSectionHeader, SkillBar, SectionModule, ... │
│ (neutral header bricks -- Headline, Subheadline, ContactLine, │
│ Masthead, SvgGlyph -- live in core.identity) │
└────────────────────────────────────────────────────────────────────────────┘
│ delegate to
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ components/ CV section renderers │
│ SectionDispatcher, EntryRenderer, RowRenderer, ParagraphRenderer │
└────────────────────────────────────────────────────────────────────────────┘
│ render into
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ data/ CvDocument, CvIdentity, CvSection (sealed), │
│ ParagraphSection / SkillsSection / RowsSection / EntriesSection, │
│ CvRow, CvEntry, Slot -- pure records, zero rendering deps │
└────────────────────────────────────────────────────────────────────────────┘
...all built on the shared, family-neutral core (templates.core.*):
┌────────────────────────────────────────────────────────────────────────────┐
│ core.theme BrandTheme = Palette / Typography / Spacing / Decoration │
│ core.identity Headline, Subheadline, ContactLine, Masthead, SvgGlyph │
│ core.text MarkdownText, RichParagraphRenderer, TextStyles │
└────────────────────────────────────────────────────────────────────────────┘
What each layer is for (in plain English):
| Layer | "Answers the question…" |
|---|---|
data/ |
"What goes on the page?" — content, no styling. |
core.theme |
"How does it look?" — colours, fonts, glyphs (shared BrandTheme). |
components/ |
"How is one element drawn?" — paragraph, row, entry primitives. |
widgets/ |
"Which visual building block do I want here?" — named LEGO bricks. |
presets/ |
"In what order, with which widgets, on which page flow?" — composition. |
Lower layers don't know about higher ones. A theme change doesn't touch a renderer. A new widget doesn't touch the data model.
| You want to… | Read |
|---|---|
| Render your own CV with your data | using-templates.md |
| Make a new visual style | authoring-presets.md |
| Add a new template family to the library (invoice, cover-letter) | contributor-guide.md |
| See hands-on recipes (change a bullet, swap colours, …) | cv/AUTHORS.md |
| Run the shipped examples | examples/cv/v2/ |