freehire is an open-source job aggregator that directly crawls company career pages. Its architecture features a unique approach to background tasks using run-once workers and transactional outboxes with PostgreSQL, avoiding long-lived daemons. It leverages Meilisearch for efficient search indexing, storing full job payloads to minimize database round trips, and integrates Go, Redis, and S3-compatible storage.
Read original on Dev.to #architecturefreehire's most distinguishing architectural decision is its use of run-once workers instead of traditional long-lived job queue daemons for background tasks. This design minimizes the operational overhead of managing background runtimes. The system relies on PostgreSQL for coordination, utilizing transactional outboxes to ensure data consistency and reliability. When a worker completes a task, it writes its result and queues follow-up actions within the same database transaction. This atomic operation prevents data loss or inconsistencies if a worker crashes, as the entire operation is rolled back or committed together.
Transactional Outbox Pattern
The transactional outbox pattern is a robust way to ensure atomicity between an application's database state and messages sent to other services or queues. By writing the event/message to an "outbox" table within the same transaction as the primary data change, you guarantee that either both succeed or both fail. A separate process then reads from the outbox table and dispatches the messages.
Each ATS provider (e.g., Greenhouse, Lever) has its own configuration file, and the `cmd/ingest` worker processes one such file per run. This isolation means that slow-crawling platforms do not impede faster ones, enhancing the overall resilience and throughput of the ingestion pipeline. The system also implements a deduplication strategy using `jobs.UNIQUE (source, external_id)` in Postgres, allowing re-crawls to simply update a `last_seen` timestamp without re-indexing unchanged content.
The search path is optimized for performance by storing the full job payload directly within Meilisearch documents. This eliminates the need for a separate database round trip to fetch job details after a search, significantly speeding up response times for search result pages. While descriptions are capped in the index for size management, the detail endpoint retrieves the full text from PostgreSQL. The system enforces deep pagination limits to prevent performance degradation.
// internal/search/searchdrain — search index document embeds the full job view
type JobDocument struct {
jobview.Job // the same JSON shape the API returns
// ...index-specific fields
}Instead of pushing individual changes to Meilisearch from many processes, the outbox pattern consolidates numerous small updates into fewer, larger pushes. This is crucial because Meilisearch re-merges its inverted index across the *entire* live index on every push. By reducing the frequency and increasing the batch size of these pushes, freehire mitigates disk I/O saturation and maintains search index performance, which was a significant issue in earlier designs with ~169 independent per-board processes.
The in-app assistant runs directly in the same process as the HTTP server. This design choice simplifies deployment and enhances security by avoiding external agent runtimes, shell access, or separate credential management. Tools called by the assistant receive only the session owner's user ID and interact with the same Go services as regular HTTP handlers, limiting potential attack vectors like prompt injection by restricting outbound channels and disallowing actions like sending emails directly.