You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
⚠️ Experimental — Pre-1.0. APIs may change between releases. Not yet recommended for production without thorough in-house testing.
mako is the open-source market-operations platform for the German energy market: every
regulated process — market communication, metering data, settlement, billing — modeled as a
correct, auditable, event-sourced workflow, for every market role (NB, LF, MSB, ESA), from
raw EDIFACT bytes to production microservices. In a sector of closed suites facing the
IS-U sunset, mako is the only end-to-end platform whose source you can read, verify, and
extend — built for the regulatory pace (LFW24, §14a, §41a, §42b/c, the EDIFACT→API
transition) that batch-era systems strain under. The domain layer is deliberately split
from transport and format so that when the market moves — MaBiS-Hub, the EDIFACT→API
target landscape, European harmonization — mako moves with a codegen run, not a rewrite.
The workspace covers the full BDEW MaKo stack across four layers:
Layer
What it is
Protocol
edi-energy EDIFACT · dvgw-edi DVGW gas · redispatch-xml Redispatch 2.0 · mako-engine event-sourced process runtime · makod daemon
makotest — Python toolkit over the same Rust core: BDEW identifier check digits, Werktag/Fristen arithmetic, AHB-validated EDIFACT, seeded EPEX curves, and a pytest plugin (README)
GeLi Gas 3.0 workflows — UTILMD G supplier-switch Gas (44001–44021) + Stammdatenänderung (44109–44182 — change families: Zustimmung/Ablehnung E15/E13/E17, Monatserster rule for bilanzierungsrelevante changes; Anfrage families G8–G10 auto-answer with a data-return of the requested MaLo master data) + INVOIC 31011 (Rechnung sonstige Leistung, AWH Sperrprozesse Gas)
Redispatch 2.0 XML/XSD format parsing — all 9 document types
energy-api
BDEW API-Webdienste Strom — REST/WebSocket client + Axum server for iMS processes
mako-markt
Master data library — MaloId, MeloId, MarktpartnerId, repository traits (incl. LokationszuordnungRepository, TechnischeRessourceRepository), CloudEvents, test doubles
Settlement, Billing & Calculation Crates
Crate / service
Purpose
grid-billing
Role-neutral German grid settlement engine — settle_nne, settle_mmm, settle_msb, settle_gas_awh, reverse, correct; returns SettlementResult/InvoiceDocument; every position carries CalculationTrace with LegalReferences (StromNEV §17/§21, GasNEV §14, KAV §2, §14a EnWG, ARegV) and TariffSource; Sparte drives Gas vs. Strom legal refs; KaKundengruppe annotates the KAV tier; regime turnovers enforced (ensure_berechenbar refuses AgNeS-era settlements); zero I/O; BO4E only via the opt-in bo4e feature (grid_billing::bo4e::into_rechnung)
MaloId (11-digit BDEW check-digit), MeloId (DE+31-char), MarktpartnerId (13-digit; auto-derives NAD DE3055 agency code 293/332/9 from prefix)
🗂️ 29 repository traits
One trait per aggregate — MaloRepository, MeloRepository, ContractRepository, PartnerRepository, LokationszuordnungRepository, TechnischeRessourceRepository, SteuerbareRessourceRepository, CorrelationIndex, … — AFIT, no dyn Trait overhead
⏳ Temporal role assignments
Rollenzuordnung with valid_from/valid_to — evaluated against CET/CEST German calendar date at query time
📨 CloudEvents 1.0
Outbound events (MarktEvent) with HMAC-SHA256 signing; InboundMakoEvent for receiving makod lifecycle events
🧪 testing feature
InMemory* test doubles for every repository trait — no PostgreSQL required in unit tests
🚫 Zero framework deps
No axum, sqlx, or async runtime — pure domain library; all I/O lives in services/marktd
BO4E typed API (marktd)
64 active rubo4e::current types — schema validated at every read/write boundary.
Category
Detail
📦 Typed responses
GET /api/v1/malo → Marktlokation; GET /api/v1/melo → Messlokation; GET /api/v1/zaehler → Zaehler; GET /api/v1/geraete → Geraet — all canonical BO4E camelCase
🔍 Schema validation on write
PUT endpoints reject wrong _typ with 422; validate enum fields (bilanzierungsmethode, netzebene, vertragsart, …) against rubo4e::current types
📋 Vertrag for LRV exchange
nb_contracts stores full BO4E Vertrag JSONB + typed SQL columns; PUT /api/v1/nb-contracts validates vertragsart / vertragsstatus; emits de.markt.nb-contract.updated CloudEvent
👤 Geschaeftspartner typed partners
PUT /api/v1/partners/{mp_id} validates the BO4E Geschaeftspartner payload (auto-injects _typ; validates marktrolle, rollencodetyp, marktteilnehmerstatus, adresse). GET returns the typed geschaeftspartner field.
🔢 Zaehlwerk register access
GET /api/v1/zaehler/{id}/zaehlwerke → Vec<Zaehlwerk> — OBIS registers for TOU billing and iMSyS demand management
⏰ ZaehlzeitRegister + ZaehlzeitSaison
GET/PUT /api/v1/zaehler/{id}/register + /zaehler-register/{id}/saisons — iMSys TOU register definitions (HT/NT/EINZEL); GET /api/v1/zaehler/{id}/tariff-zone?datetime=ISO resolves zone in one SQL JOIN (§14a Modul 2)
⚡ Energiemenge deliveries
GET /api/v1/deliveries/{malo_id} → Vec<Energiemenge> — typed ERP-consumable meter readings without EDIFACT parsing
💰 MMMA settlement prices
GET/PUT /api/v1/mmma-preise/gas/{year}/{month} — Gas MMM Abrechnungspreise (Trading Hub Europe); GET/PUT /api/v1/mmm-preise/strom/{year}/{month} — Strom MMM Ausgleichsenergie per ÜNB. Both auto-fetched by netzbilanzd and validated by invoicd check 6.
🗂️ Fallgruppe + Bilanzierungsmethode auto-extract
makod adapters extract bilanzierungsmethode (Z01→SLP, Z02→RLM, Z04→IMS) and fallgruppe (GaBi Gas, TM+Z10) from UTILMD TM+EM / TM+Z10 segments. marktdevent_ingest calls patch_typenmerkmal() on de.mako.process.initiated (PIDs 55001/44001) to keep malo.fallgruppe / malo.bilanzierungsmethode in sync.
🧾 Steuerbetrag + Registeranzahl
energy-billing projects the EN 16931 BG-23 tax breakdown into BO4E Steuerbetrag entries on the Rechnung JSON; Registeranzahl (Eintarif/Zweitarif) drives HT/NT position branching
🏦 Zahlungsinformation + Zahlungsart
accountingd SEPA mandate registry stores structured payment info; pain.008 XML generated from SepaMandateRow (IBAN, BIC, Kontoinhaber, Mandatsreferenz)
Process engine layer (mako-engine + domain crates)
Category
Detail
♻️ Event-sourced processes
Optimistic-concurrency event append with SlateDB-backed storage
⚛️ Atomic dual-write
Events and outbox messages written in a single WriteBatch via AtomicAppend
⏰ Regulatory deadlines
DeadlineStore with GPKE 24h / WiM 5-Werktage / GeLi Gas 10-Werktage Fristen
📨 AS4 inbound transport
makod receives BDEW AS4 pushes via asx-rs, deduplicates with SlateDbInboxStore, routes by Pruefidentifikator
🔐 Cedar ABAC authorization
All HTTP endpoints gated by Cedar attribute-based access control; built-in default policy with custom policy overlay via --cedar-policy-dir
🪪 OIDC / JWT + API-key auth
JWT bearer tokens from Azure AD, Keycloak, Okta, Kubernetes workload identity; RS256/ES256/PS256 families only; JWKS cached with background refresh; coexists with named API keys
📡 CloudEvents 1.0 ERP webhooks
Outbound ERP notifications as CloudEvents 1.0 structured-mode JSON (application/cloudevents+json), HMAC-SHA256 signed; natively routable by SAP BTP, AWS EventBridge, Azure Event Grid, Google Eventarc
🔄 Format-version coexistence
Processes started under FV2025-10-01 run to completion under those rules even after FV2026-10-01 cutover
[dependencies]
mako-engine = { version = "0.13", features = ["testing"] }
mako-gpke = "0.13"
use mako_engine::{
builder::EngineBuilder,
ids::TenantId,
version::WorkflowId,
event_store::InMemoryEventStore,};use mako_gpke::lf_anmeldung::GpkeLfAnmeldungWorkflow;let ctx = EngineBuilder::new().with_event_store(InMemoryEventStore::new()).build();// Spawn a new process for one delivery point.let process = ctx.spawn::<GpkeLfAnmeldungWorkflow>(TenantId::new(), wf_id);let envelopes = process.execute(initiate_cmd).await?;// Reconstruct typed state by replaying all persisted events.let state = process.state().await?;
use redispatch_xml::{parse_and_validate, serialize, detect,DocumentType};// Optionally detect document type before parsing (useful for routing)let doc_type = detect(xml_bytes);// Parse + validate in one step (recommended)let doc = parse_and_validate(xml_bytes)?;// Primary routing keys — use to correlate AcknowledgementDocument to processprintln!("mRID: {}", doc.mrid());println!("sender: {}", doc.sender_id());// EIC of TSO/RSOprintln!("receiver: {}", doc.receiver_id());// Serialize back to XML (byte-stable round-trip)let out = serialize(&doc)?;
🚀 Quick Start — Master data (mako-markt)
[dependencies]
mako-markt = { version = "0.13", features = ["testing"] }
use mako_markt::domain::{MaloId,MeloId,MarktpartnerId};// Validated identifiers — construction returns Err on malformed inputlet malo_id = MaloId::new("51238696780")?;let melo_id = MeloId::new("DE0001234567890123456789012345678")?;let mp_id = "9900357000004".parse::<MarktpartnerId>()?;// NAD DE3055 agency code derived from MP-ID prefix automatically:// "99…" → "293" (BDEW Strom), "98…" → "332" (DVGW Gas), other → "9" (GS1)assert_eq!(mako_markt::domain::nad_agency_code(&mp_id),"293");// In tests — use InMemory* doubles; no PostgreSQL requireduse mako_markt::testing::InMemoryMaloRepository;let repo = InMemoryMaloRepository::default();
📋 Format and Document Coverage
BDEW EDI@Energy (edi-energy) — 17 EDIFACT message types
Message
EDIFACT type
Latest release
Use case
UTILMD Strom
UTILMD
S2.2 (fv20261001)
Grid connection (supplier switch, registration)
UTILMD Gas
UTILMD
G1.2 (fv20261001_gas)
Gas grid connection processes
MSCONS
MSCONS
2.5 (fv20261001)
Metered services consumption reports
APERAK
APERAK
2.2 (fv20261001)
Application error acknowledgements
CONTRL
CONTRL
2.0b (fv20260101)
Interchange control acknowledgements
INVOIC
INVOIC
2.8e (fv20260401)
Invoices
REMADV
REMADV
2.9f (fv20260401)
Remittance advice
ORDERS
ORDERS
1.4b (fv20260401)
Purchase orders
IFTSTA
IFTSTA
2.1 (fv20261001)
Multimodal status reports
INSRPT
INSRPT
1.1a (fv20260101)
Inspection reports
REQOTE
REQOTE
1.3c (fv20260401)
Requests for quotation
PARTIN
PARTIN
1.1 (fv20260401)
Party information
ORDCHG
ORDCHG
1.2 (fv20260401)
Purchase order changes
ORDRSP
ORDRSP
1.4c (fv20260401)
Purchase order responses
QUOTES
QUOTES
1.3c (fv20260401)
Quotations
COMDIS
COMDIS
1.0h (fv20261001)
Commercial dispute (Handelsunstimmigkeit)
PRICAT
PRICAT
2.1 (fv20260401)
Price/sales catalogue
UTILTS
UTILTS
1.1e (fv20260401)
Technical master data
DVGW gas transport (dvgw-edi) — 8 message types
Message
Version
Direction
Use case
ALOCAT
5.11a
FNB/MGV/VNB → BKV
Gas quantity allocation list
NOMINT
4.6 FK
BKV → FNB/MGV
Nomination submission
NOMRES
4.7 FK
FNB/MGV → BKV
Nomination response / matching result
SCHEDL
G685/G2000
FNB → BKV
Transport schedule
IMBNOT
G685/G2000
FNB/MGV → BKV
Intraday imbalance notification
TRANOT
G685/G2000
FNB/VNB → BKV/GH/MGV
Transport restriction / event notification
DELORD
G685/G2000
BKV → FNB
Delivery order (quantity nomination)
DELRES
G685/G2000
FNB → BKV
Delivery order confirmation / rejection
Redispatch 2.0 XML (redispatch-xml) — 9 document types
By default UTILMD, MSCONS, APERAK, and CONTRL are compiled in:
[dependencies]
edi-energy = { version = "0.13", features = ["invoic", "remadv", "orders"] }
Flag
Default
Enables
utilmd
✅
UTILMD Strom + Gas
mscons
✅
MSCONS metered consumption
aperak
✅
APERAK error acknowledgement
contrl
✅
CONTRL syntax acknowledgement
invoic
INVOIC invoice
remadv
REMADV remittance advice
orders
ORDERS purchase order
iftsta
IFTSTA multimodal status
insrpt
INSRPT inspection report
reqote
REQOTE request for quotation
partin
PARTIN party information
ordchg
ORDCHG order change
ordrsp
ORDRSP order response
quotes
QUOTES quotation
comdis
COMDIS commercial dispute
pricat
PRICAT price catalogue
utilts
UTILTS technical master data
archive
All archived profiles (expired release windows)
serde
Serialize on EdiEnergyReport
diagnostics
miette::Diagnostic on reports
tracing
Structured tracing spans
⚙️ Feature Flags — dvgw-edi
All 8 format parsers are compiled in by default. Disable unused formats to reduce binary size:
dvgw-edi = { version = "0.13", default-features = false, features = ["nomint", "nomres"] }
Flag
Default
Enables
alocat
✅
AlocatMessage and ALOCAT parsing
nomint
✅
NomintMessage and NOMINT parsing
nomres
✅
NomresMessage and NOMRES parsing
schedl
✅
SchedlMessage and SCHEDL parsing
imbnot
✅
ImbalanceMessage and IMBNOT parsing
tranot
✅
TransportNotificationMessage and TRANOT parsing
delord
✅
DeliveryOrderMessage and DELORD parsing
delres
✅
DeliveryResponseMessage and DELRES parsing
decimal
✅
Decimal-based quantity_decimal() on AlocatQuantity (preferred for billing)
serde
Serialize/Deserialize on all public types
tracing
Structured tracing spans during parse dispatch
⚙️ Feature Flags — mako-markt
Flag
Default
Enables
(default)
✅
All domain types, all repository traits, CloudEvents, InboundMakoEvent
testing
InMemory* test doubles for every repository trait — never enable in production
⚙️ Feature Flags — mako-engine / makod
Flag
Crate
Enables
slatedb
mako-engine
Production SlateDbStore; activated in makod via its dep on mako-engine = { features = ["slatedb"] } — never enable in library [features] defaults
testing
mako-engine
InMemoryEventStore, NoopDeadLetterSink, InMemoryInboxStore — never in production
tracing
mako-engine
Structured instrumentation spans
🔧 Development
The justfile is the front door — every gate below has a recipe:
just # list all recipes
just check # cargo check, all targets & features
just test# full test suite
just ci # the complete CI gate (check + test + clippy + fmt + deny + codegen/profile/PID validation)
just test-db # every real-PostgreSQL integration suite (testcontainers)
just test-accountingd-db # …or one at a time: edmd, einsd, accountingd, billingd, vertragd, tarifbd, marktd
The test-*-db suites self-manage PostgreSQL via testcontainers — a throwaway
postgres:17-alpine container is started in-process and reaped afterwards, so the only
requirement is a running Docker daemon (no manual docker run, no DATABASE_URL). They
are #[ignore]d by default and skip gracefully when Docker is absent.
Raw cargo equivalents:
# Check all targets — minimum gate before any commit
cargo check --all-targets --all-features
# Run all tests
cargo test --all-features
# Run tests for one crate
cargo test -p mako-engine --all-features
# Build the production daemon (slatedb is already enabled via mako-engine dep in Cargo.toml)
cargo build -p makod --release
# Lint (warnings are errors)
cargo clippy --all-targets --all-features -- -D warnings
# Format
cargo fmt --all
# Dependency audit (license + security)
cargo deny check
# Validate all profile JSON against JSON Schema
cargo xtask validate-profiles
# Check that every Pruefidentifikator has a test fixture
cargo xtask validate-pruefids
# Check that today's date is covered by a current profile
cargo xtask check-release-coverage
# Regenerate all profile Rust code after editing profiles/
cargo xtask codegen
# Check no generated code has drifted
cargo xtask codegen --check
# Compute a diff between two annual releases
cargo xtask release-diff --from utilmd/fv20251001 --to utilmd/fv20261001
# Run fuzz target (requires nightly + cargo-fuzz)
cargo +nightly fuzz run fuzz_parse_validate
📊 Performance — edi-energy
Benchmarks on Apple M-series (single core, Criterion):
Operation
Throughput
Parse minimal UTILMD
~2 µs / message
Validate UTILMD S2.1 (MIG + AHB)
~8 µs / message
Parse 100-message interchange
~180 µs total
Build UTILMD + serialize
~5 µs / message
cargo bench --bench benchmarks
🤝 Contributing
Contributions are welcome. Open an issue before large changes.
Run cargo check --all-targets --all-features and cargo test --all-features before submitting a PR.
Generated files under crates/edi-energy/src/generated/ are machine-produced — edit the profile JSON and run cargo xtask codegen instead.