RFQ automation: designing a supplier quoting system
TL;DR
- RFQ automation is mostly about workflow correctness: deadlines, versions, compliance, and audit trails.
- Use a workflow engine pattern (explicit states + transitions), plus an event log for traceability.
- Model quotes as versioned snapshots to handle revisions without losing history.
- Build scoring as a pluggable rules pipeline so procurement can change weighting without redeploys.
- Expect gotchas around time zones, partial compliance, attachments, and supplier identity.
The problem: why email + Excel breaks under load
Procurement flows look simple on a whiteboard: create an RFQ, invite suppliers, collect offers, compare, pick, then issue a PO. The pain shows up when the system must answer questions like:
- “Which suppliers received version 3 of the spec?”
- “Who answered before the deadline, and what changed after clarification?”
- “Why was supplier X rejected—price, lead time, missing compliance docs?”
- “Can we replay the decision during an audit?”
That’s where RFQ automation becomes a technical problem: build a system that captures decisions as data, not as tribal knowledge scattered across inboxes.
For context, SCF (Sistem de Cotare Furnizori) on sistemcotarefurnizori.ro describes an end-to-end flow: campaigns, supplier invitations, structured replies, comparisons, and automated ordering. This post focuses on how you’d implement that kind of platform from an architecture standpoint.
Solution shape: a workflow-first architecture
If you treat RFQs as “just CRUD,” you’ll end up encoding business rules in controllers and ad-hoc SQL. A better approach is to design around a state machine and domain events.
Core components
- RFQ Service: owns RFQ lifecycle, deadlines, invitations.
- Supplier Portal/API: authentication, quote submission, attachment upload.
- Scoring Service: computes comparable metrics and weighted scores.
- Ordering/ERP Integration: turns a winning quote into a PO.
- Audit Log/Event Store: immutable ledger of what happened.
A small team can implement this as a modular monolith; the key is boundaries and explicit contracts.
Data model: versioned quotes + immutable events
The most expensive production bug in RFQ automation is “we can’t explain how we got here.” Solve that with an append-only event log and versioned entities.
Suggested relational model (minimal)
rfq(id, title, status, created_at, closes_at, buyer_org_id)rfq_item(id, rfq_id, sku, description, qty, uom)supplier(id, legal_name, vat_id, risk_tier, active)invitation(id, rfq_id, supplier_id, sent_at, acknowledged_at)quote(id, rfq_id, supplier_id, current_version, submitted_at)quote_version(id, quote_id, version, payload_json, created_at, submitted_by)attachment(id, owner_type, owner_id, storage_key, sha256, uploaded_at)event_log(id, aggregate_type, aggregate_id, event_type, event_json, occurred_at, actor)
Use quote_version.payload_json to store a normalized snapshot (prices per item, lead times, Incoterms, validity date, compliance flags). This keeps schema stable while the payload evolves.
Event log example
{
"aggregate_type": "rfq",
"aggregate_id": "RFQ-1042",
"event_type": "QuoteSubmitted",
"occurred_at": "2026-02-26T10:14:22Z",
"actor": "supplier:SUP-88",
"event_json": {
"quoteId": "Q-9001",
"version": 2,
"items": 12,
"total": 48320.50,
"currency": "EUR"
}
}
If you’ve never implemented a state machine, start with a small transition table. The State pattern is a useful mental model even if you don’t literally implement OO states.
Workflow engine pattern: states, transitions, and invariants
A robust RFQ automation implementation encodes “what can happen next” centrally.
Example RFQ states
DRAFTPUBLISHEDCLARIFICATIONCLOSEDEVALUATINGAWARDEDCANCELLED
Transition rules (simplified)
-
DRAFT -> PUBLISHEDonly if items exist and closes_at in the future -
PUBLISHED -> CLARIFICATIONif buyer posts Q&A -
PUBLISHED -> CLOSEDautomatically at closes_at -
CLOSED -> EVALUATINGwhen evaluation starts -
EVALUATING -> AWARDEDwhen winner selected
Pseudocode for a transition guard
type RfqStatus = 'DRAFT'|'PUBLISHED'|'CLARIFICATION'|'CLOSED'|'EVALUATING'|'AWARDED'|'CANCELLED'
function canTransition(from: RfqStatus, to: RfqStatus, rfq: Rfq): boolean {
if (from === 'DRAFT' && to === 'PUBLISHED') {
return rfq.items.length > 0 && rfq.closesAt > new Date()
}
if (from === 'PUBLISHED' && to === 'CLOSED') {
return new Date() >= rfq.closesAt
}
// ...
return false
}
This is where you enforce invariants like “no submissions after close” (or allow them but mark as late).
Collecting offers: idempotency, retries, and attachments
Supplier submissions are a classic “flaky network” scenario. In RFQ automation, you need idempotent APIs.
Submission endpoint essentials
- Require an
Idempotency-Keyheader per supplier per quote version. - Store payload first, then emit events.
- Validate attachments by hash to prevent duplicates.
POST /api/rfqs/RFQ-1042/quotes
Idempotency-Key: 6f8b3a0c-1a9c-4c2c-a2b2-7c3a6d...
Content-Type: application/json
{ "supplierId": "SUP-88", "items": [ ... ], "validUntil": "2026-03-15" }
For files, use pre-signed uploads (S3/GCS/etc.) and store sha256 + metadata in your DB.
Comparing offers: scoring as a rules pipeline
A “lowest price wins” approach is rarely accurate. A practical RFQ automation setup computes a score from multiple dimensions.
Dimensions you can quantify
- Price (normalized across currencies)
- Lead time (days)
- Payment terms (e.g., Net 30 vs Net 60)
- Compliance (binary/weighted)
- Supplier performance (OTD, defect rate)
Rules pipeline example
def score(quote, weights):
# normalize
price_score = 100 * (min_price / quote.total_price)
lead_score = max(0, 100 - quote.lead_days * 2)
compliance = 100 if quote.compliant else 0
return (
weights['price'] * price_score +
weights['lead'] * lead_score +
weights['compliance'] * compliance
)
Store weights per category or per RFQ. If you want procurement to tune scoring safely, validate that weights sum to 1.0 and version them.
For domain grounding, the RFQ concept and terminology is well summarized on Wikipedia’s Request for quotation page. It’s not an implementation guide, but it’s a good shared vocabulary across teams.
Automation after award: from quote to purchase order
The moment you award, RFQ automation should produce a deterministic “award snapshot” that can be handed to ERP.
Award snapshot (immutable)
- Winning supplier + quote version
- Selected line items + agreed prices
- Delivery addresses
- Incoterms, payment terms
- Required documents
Then integrate via:
-
Outbox pattern: write
PurchaseOrderCreatedto DB, ship to message broker - Webhook to ERP middleware
- Direct API to ERP if available
If you’re implementing this in a monolith, a transactional outbox table avoids “DB committed but message not sent.” The Outbox pattern write-up is a solid reference.
Gotchas: what breaks first in production
Time zones and deadlines
If suppliers are in multiple countries, “close at 17:00” must be explicit:
- Store timestamps in UTC.
- Display in user’s locale.
- Lock submissions using server time, not client time.
Quote revisions vs. overwrites
Never overwrite a quote. In RFQ automation, revisions are normal. Keep versions and allow the buyer to compare v1 vs v2.
Partial compliance
Suppliers might be compliant for some items but not others. Model compliance per line item, not only at quote level.
Identity and duplicates
Suppliers may exist twice (different emails, similar legal names). Use VAT/tax IDs and a dedupe workflow.
What I learned building systems like this
- Make “why” queryable: if your UI shows a decision, your DB should be able to explain it.
- Design for audit from day one: bolt-on audit logs are always incomplete.
- Scoring must be explainable: show a breakdown, not just a number.
- Events are your debugging superpower: replaying flows beats guessing.
Helpful next step
If you’re working on procurement tooling or evaluating a platform approach, sketch your RFQ state machine and event types first, before choosing frameworks. If you want, share your current RFQ statuses and pain points in the comments—I can suggest a transition model and a minimal event schema that fits your domain.
Originally about RFQ automation.
Top comments (0)