Understanding Domain-Driven Design
A comprehensive introduction to Domain-Driven Design principles and how to apply them in modern software development...
text-primary #2B2B2B Body text text-secondary #595959 Meta text-muted #8a8a8a De-emphasized accent #7A5C3A Links accent-hover #5d462c Link hover accent-disabled #b8a594 Disabled bg-white #ffffff Main background bg-beige #f2e5d7 Sections bg-subtle #E8E4DC Subtle distinction bg-dark #2d2d2d Footer surface-muted #F4F1ED Cards/code code-bg rgba(122, 92, 58, 0.15) Inline code code-text #5a4632 Inline code text border #E8E4DC Borders text-on-dark #faf8f5 Text on dark bg Brighter default series colors for markdown table graphs. They stay within the warm editorial brand, but separate more clearly than the initial brown-heavy set.
chart-series-1 #4E7EA6 Default graph series 1 chart-series-2 #C45B4D Default graph series 2 chart-series-3 #5E8B4A Default graph series 3 chart-series-4 #B86A3B Default graph series 4 chart-series-5 #2E7D73 Default graph series 5 chart-series-6 #D08A2F Default graph series 6 --font-serif (headings)
The quick brown fox jumps over the lazy dog
Georgia, "Times New Roman", serif --font-sans (body text)
The quick brown fox jumps over the lazy dog
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif --font-mono (code)
The quick brown fox jumps over the lazy dog
ui-monospace, SFMono-Regular, Menlo, Monaco, monospace Body text with link and inline code.
A blockquote example.
Default table
| Month | Total Jobs | False Negatives | True Positives |
|---|---|---|---|
| Jan 2025 | 56,573 | 693 | 1,060 |
| Feb 2025 | 53,118 | 613 | 907 |
| Mar 2025 | 49,218 | 596 | 1,144 |
Condensed table marker
<!-- table: condensed -->
| Month | Total Jobs | False Negatives | True Positives |
| --- | ---: | ---: | ---: |
| Jan 2025 | 56,573 | 693 | 1,060 |
| Feb 2025 | 53,118 | 613 | 907 |
| Mar 2025 | 49,218 | 596 | 1,144 | Rendered condensed preview
| Month | Total Jobs | False Negatives | True Positives |
|---|---|---|---|
| Jan 2025 | 56,573 | 693 | 1,060 |
| Feb 2025 | 53,118 | 613 | 907 |
| Mar 2025 | 49,218 | 596 | 1,144 |
Use <!-- table: condensed --> immediately above a markdown table, with no paragraph text in between.
<!-- table: graph line caption="Biweekly F1 trend for LLM vs ML" colors="#7A5C3A,#516C63" -->
<!-- table: condensed -->
| Period | LLM F1 | ML F1 |
| --- | ---: | ---: |
| Jul 2025 | 88% | 82% |
| Aug 2025 | 90% | 84% |
| Sep 2025 | 86% | 83% |
| Oct 2025 | 91% | 85% |
| Nov 2025 | 93% | 86% |
| Dec 2025 | 95% | 87% | | Period | LLM F1 | ML F1 |
|---|---|---|
| Jul 2025 | 88% | 82% |
| Aug 2025 | 90% | 84% |
| Sep 2025 | 86% | 83% |
| Oct 2025 | 91% | 85% |
| Nov 2025 | 93% | 86% |
| Dec 2025 | 95% | 87% |
Use <!-- table: graph line --> above a markdown table to render a static line chart while retaining the original table below. The first column becomes x-axis labels, all remaining columns must be numeric, caption="..." is optional, and colors="#7A5C3A,#516C63" maps left-to-right to the plotted series columns.
## Appendix: Commercial lease extraction fields
These are the rules the extraction prompt used for the most business-sensitive lease fields.
<!-- table: condensed -->
| Field | Full form | Extraction rules |
| --- | --- | --- |
| `year` | Lease Year | If the term is `2024-2025`, return `2025`. Never return empty or zero. |
| `base_rent` | Base Rent | Exclude CAM and parking. If monthly, multiply by 12. Return `0` if missing. |
| `notes` | Notes | Capture `$XX.XX/sqft` rates and flags like `Sublessee` or `Month-to-Month`. | These are the rules the extraction prompt used for the most business-sensitive lease fields.
| Field | Full form | Extraction rules |
|---|---|---|
year | Lease Year | If the term is 2024-2025, return 2025. Never return empty or zero. |
base_rent | Base Rent | Exclude CAM and parking. If monthly, multiply by 12. Return 0 if missing. |
notes | Notes | Capture $XX.XX/sqft rates and flags like Sublessee or Month-to-Month. |
Use a level-2 heading that starts with Appendix: when a post needs expandable reference material at the end without interrupting the main reading flow.
def update
access_token = find_access_token
if access_token.present? && !access_token.expired?
redirect_to profile_url
else
render :expired_token
end
end Conversation traces rendered from markdown ```claude fences, including tool calls, results, and separators.
```claude title="Investigating Timeline Slowdown"
human [t=0:00]: show me why /ops/case_packets/4621 is slow
claude [t=0:04]: I traced the request and found three expensive queries.
---
1. Unscoped audit query
2. AxComment bulk load
3. JSONB LIKE scan
tool [t=0:12]: db_query [ops-db]
'''sql
SELECT id, duration_ms
FROM audit_entries
WHERE action_name = 'remove';
'''
result [t=0:13]:
'''text
27→ AuditTrail::Entry Load (293.5ms)
28→ AxComment Load (4.5ms)
29→ Rendered comments partial (174.0ms)
'''
tool [t=0:19]: askuserquestion
'''json
{"questions":[{"header":"DB Migration","question":"Should the cleanup drop `document_audit_records`, or only remove application code?","options":[{"label":"Drop the table (Recommended)","description":"Create a migration to drop the table and fully clean up the schema."},{"label":"Code-only removal","description":"Remove the feature code but leave the table in place so the data remains available."}],"multiSelect":false},{"header":"Verification","question":"Which checks should we run before merging the cleanup?","options":[{"label":"Run the build","description":"Confirm the Astro site still compiles and the markdown renderer loads."},{"label":"Spot-check the story page","description":"Verify the generated story output shows the question cards instead of raw JSON."}],"multiSelect":true}]}
'''
result [t=0:24]:
'''text
User has answered your questions: "Should the cleanup drop `document_audit_records`, or only remove application code?"="Drop the table (Recommended)", "Which checks should we run before merging the cleanup?"="Run the build". You can now continue with the user's answers in mind.
'''
claude [t=0:31]: With those answers, I can clean up the schema and verify the renderer in one pass.
human [t=0:36]: Great, drop the table and run the build before merging.
``` show me why /ops/case_packets/4621 is slow
I traced the request and found three expensive queries.
SELECT id, duration_ms
FROM audit_entries
WHERE action_name = 'remove'; Should the cleanup drop document_audit_records, or only remove application code?
Which checks should we run before merging the cleanup?
document_audit_records, or only remove application code?"="Drop the table (Recommended)", "Which checks should we run before merging the cleanup?"="Run the build". You can now continue with the user's answers in mind.With those answers, I can clean up the schema and verify the renderer in one pass.
Great, drop the table and run the build before merging.
Markdown-native prompt cards rendered from ```prompt fences. The example below shows the current behavior: raw monospace body, wrapped long lines, optional model="..." metadata, opt-in collapsible presentation via collapsed="true", Shiki-style line highlighting via fence metadata such as {2,6,10-13}, and a top-right copy control that copies the authored prompt body as plain text.
```prompt model="gpt-4.1-mini" name="Is This Job in Cybersecurity & InfoSec?" collapsed="true" {2,6,10-13}
## TASK
Determine if an EU public sector job listing belongs to a Cybersecurity & InfoSec function.
Mark as `"infosec_department": true` only if:
- The job is explicitly assigned to "Cybersecurity," "Information Security," "CERT," "Security Operations," or a close variant;
- Or there is no department listed, and the listing includes strong cybersecurity signals such as NIS2, DORA, ENISA, CSIRT, EUCI, SOC analyst, CERT incident handler, security engineer, penetration tester, threat intelligence analyst, vulnerability management, SIEM operations, security architect, cyber risk analyst, digital forensics, security operations centre, and eIDAS trust services in a clearly cybersecurity-specific context;
- If the job is clearly assigned to a **non-cybersecurity department** such as IT Helpdesk & Desktop Support, Data Protection & Privacy, or Property & Building Management, return `false` even if keywords appear.
---
## OUTPUT
Return only this JSON:
'''json
{
"infosec_department": true | false,
"reasoning": "10-word concise justification"
}
'''
``` Use the toggle to collapse long reference prompts without moving them into an appendix. The copy button still grabs the original prompt body exactly as authored inside the fence, without the surrounding markdown fence markers.
## TASK
Determine if an EU public sector job listing belongs to a Cybersecurity & InfoSec function.
Mark as `"infosec_department": true` only if:
- The job is explicitly assigned to "Cybersecurity," "Information Security," "CERT," "Security Operations," or a close variant;
- Or there is no department listed, and the listing includes strong cybersecurity signals such as NIS2, DORA, ENISA, CSIRT, EUCI, SOC analyst, CERT incident handler, security engineer, penetration tester, threat intelligence analyst, vulnerability management, SIEM operations, security architect, cyber risk analyst, digital forensics, security operations centre, and eIDAS trust services in a clearly cybersecurity-specific context;
- If the job is clearly assigned to a **non-cybersecurity department** such as IT Helpdesk & Desktop Support, Data Protection & Privacy, or Property & Building Management, return `false` even if keywords appear.
---
## OUTPUT
Return only this JSON:
'''json
{
"infosec_department": true | false,
"reasoning": "10-word concise justification"
}
'''
Ticket templates and adjacent planning snippets rendered from ```story fences. Use explicit type="chore", type="bug", type="feature", type="non-user-feature", or type="spike" for typed cards, or label="Hypothesis"-style metadata when you want the same card treatment for generic text sections. Each card now includes a copy control that copies the authored ticket body as plain text.
```story type="chore"
Consolidate story template rendering
## Description
We should replace raw ticket template fences with styled story cards
Before we publish more backlog-writing examples in the blog
So that the templates read like executable tickets instead of code samples
## Acceptance Criteria
- [ ] Top header shows the story type
- [ ] Body copy uses reading fonts instead of monospace
- [ ] Links to [/styleguide](/styleguide) still render normally
## Resources
- Current story-template post draft
- Renderer notes in `src/plugins/remark-story-card.mjs`
``` ```story label="Hypothesis"
We believe building {feature} for {user} addresses their {need/painpoint} and will achieve {outcome}, measured by {leading indicator}.
``` ```story type="non-user-feature"
Build the callback reconciliation job
As a document intake service
I want to retry callback reconciliation automatically
So that delayed provider responses do not require manual cleanup
## Structured Details
- **Actor (System/Service/Machine):** Intake callback worker
- **Endpoint(s):** Scanbridge callback webhook and retry queue
- **Failure Scenarios:** Duplicate callback payloads and missing source PDFs
``` ```story type="spike"
Assess callback retry edge cases
## Description
We should investigate callback retry behavior under partial outage conditions
Before we automate the whole recovery path
So that we know which failures need queue retries versus operator intervention
## Time Box
1 day
``` The copy button preserves the original story body line breaks and checklist syntax, but does not include the outer ```story fence.
We should replace raw ticket template fences with styled story cards
Before we publish more backlog-writing examples in the blog
So that the templates read like executable tickets instead of code samples
We believe building {feature} for {user} addresses their {need/painpoint} and will achieve {outcome}, measured by {leading indicator}.
Operators can reproduce the failure, but the follow-up retry action remains unavailable even after the record is reset.
As an operations analyst
I want to inspect override reasons inline
So that I can resolve ingestion issues without jumping between admin pages
Scenario: operator opens an item with prior overrides
Given the item has at least one timeline correction
When the operator expands the history panel
Then the override reason and updated value are both visible
As a document intake service
I want to retry callback reconciliation automatically
So that delayed provider responses do not require manual cleanup
We should investigate callback retry behavior under partial outage conditions
Before we automate the whole recovery path
So that we know which failures need queue retries versus operator intervention
1 day
Use this pattern when a blog image should break wider than body copy, matching the wider editorial treatment used for videos and prompt cards.
<figure class="extended-width-media">
<img
src="/assets/images/posts/2026-03-18-how-to-increase-recall-on-a-job-classifier-with-heuristics-ml-and-a-smarter-llm/human-in-the-loop-ui-video.avif"
alt="Human review queue with ML and LLM outputs side by side"
/>
<figcaption>Use a figure wrapper when an image should align with the wider video and prompt treatment.</figcaption>
</figure>
Blog post images only zoom when you opt in with data-zoom. Supported patterns are <img data-zoom /> and <figure data-zoom><img ... /></figure>. Plain Markdown image syntax stays non-zoomable unless you switch to raw HTML.
<!-- Opt in with data-zoom on a plain image. -->
<img
src="/assets/images/posts/2026-03-18-how-to-increase-recall-on-a-job-classifier-with-heuristics-ml-and-a-smarter-llm/heuristic-layer.avif"
alt="Layered classification pipeline diagram"
data-zoom
/>
<!-- Opt in on a wrapped image by marking the figure. -->
<figure class="extended-width-media" data-zoom>
<img
src="/assets/images/posts/2026-03-18-how-to-increase-recall-on-a-job-classifier-with-heuristics-ml-and-a-smarter-llm/human-in-the-loop-ui-video.avif"
alt="Human review queue with ML and LLM outputs side by side"
/>
<figcaption>Extended-width images can opt in at the figure level.</figcaption>
</figure>
<!-- Plain markdown syntax stays non-zoomable unless you switch to raw HTML. -->
 The first image and the extended-width figure opt in with data-zoom. The final image represents plain Markdown output and stays non-zoomable.
Used on the /topics page
<div class="featured-grid">
<a href="/topics/ruby-on-rails" class="topic-card">
<div class="topic-card-title">Ruby on Rails</div>
<div class="topic-card-count">24 posts</div>
<div class="topic-card-description">Web application framework patterns and best practices</div>
</a>
<a href="/topics/software-architecture" class="topic-card">
<div class="topic-card-title">Software Architecture</div>
<div class="topic-card-count">18 posts</div>
<div class="topic-card-description">System design and architectural decision making</div>
</a>
</div> <div class="topics-list">
<a href="/topics/mongodb" class="topic-link">
<span class="topic-name">MongoDB</span>
<span class="topic-count">12</span>
</a>
<a href="/topics/testing" class="topic-link">
<span class="topic-name">Testing</span>
<span class="topic-count">8</span>
</a>
<a href="/topics/agile" class="topic-link">
<span class="topic-name">Agile</span>
<span class="topic-count">15</span>
</a>
</div> Callout styles for highlighting content in blog posts. Use in markdown with > [!type] syntax.
> [!summary] (Top-of-post summary)> [!summary] **In short:**
>
> The legacy classifier missed too many cybersecurity jobs and generated heavy manual QA work.
>
> We replaced a single prompt chain with layered heuristics, ML, and a narrower LLM decision stage.
>
> Result: recall moved from the high 60s into the mid-90s without increasing API spend.
>
> <small>Note: Example details are simplified and anonymized for publication.</small> In short:
The legacy classifier missed too many cybersecurity jobs and generated heavy manual QA work.
We replaced a single prompt chain with layered heuristics, ML, and a narrower LLM decision stage.
Result: recall moved from the high 60s into the mid-90s without increasing API spend.
Note: Example details are simplified and anonymized for publication.Use this at the top of a post body to give readers a fast executive summary.
> [!callout] (Recommended)This is the recommended default style. It uses a thick left border with a subtle background tint. Clean, modern, and doesn't interrupt the reading flow.
> [!minimal]This variation removes the background entirely, using only the left border. Even more minimal - great for callouts that shouldn't interrupt the visual hierarchy.
> [!indent]A sophisticated typographic approach with indent and em-dash. Perfect for quotes, asides, or reflective commentary that should feel integrated into the main text.
> [!torn]The torn paper effect adds an organic, handcrafted feel. The irregular edge suggests something hand-made and personal. Use sparingly for special callouts.
> [!sketch]The sketch box mimics a hand-drawn rectangle with a slight rotation for an imperfect, human quality. Pairs well with Bikablo illustrations.
Tag pills with border, used in blog posts
A comprehensive introduction to Domain-Driven Design principles and how to apply them in modern software development...
<ShareBar
title="From 68% Recall to 95%: Fixing a Job Classifier Without Increasing LLM Spend"
url="https://teotti.com/how-to-increase-recall-on-a-job-classifier-with-heuristics-ml-and-a-smarter-llm"
/> Hover to see lift effect
Subtle letterpress text effect
<div class="header">
<nav class="header-nav max-w-content mx-auto px-6 py-6">
<div class="header-top">
<div class="header-brand">
<a href="/" class="header-logo">Enrico Teotti</a>
</div>
<button class="menu-toggle" aria-label="Toggle menu">
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>
</div>
<div class="header-links">
<a href="/blog" class="active">Blog</a>
<a href="/topics">Topics</a>
<a href="/about">About</a>
<a href="/public-speaker">Talks</a>
</div>
</nav>
</div> <footer class="footer section-dark">
<div class="footer-content max-w-content mx-auto px-6 py-12">
<div class="footer-social">
<a href="#" class="footer-social-link">Twitter</a>
<a href="#" class="footer-social-link">LinkedIn</a>
<a href="#" class="footer-social-link">GitHub</a>
<a href="/resume" class="footer-social-link">Resume</a>
<a href="/rss.xml" class="footer-social-link">RSS</a>
</div>
<div class="footer-meta">
<p class="footer-copyright">© 2008-2026 Enrico Teotti. All rights reserved.</p>
</div>
</div>
</footer> Press Tab on page load to see skip link
Select this text to see the custom selection color (accent background, white text)