Model and source
- Citation: Peng J, Huang J, Tan H, Kuang Y, Yang G, Huang Z. Model-Informed Dose Selection for a Novel Human Immunoglobulin G4 Derived Monoclonal Antibody Targeting Proprotein Convertase Kwashiorkor Type 9: Insights from Population Pharmacokinetics-Pharmacodynamics and Systems Pharmacology. ACS Pharmacol Transl Sci. 2024;7(2):406-420. doi:10.1021/acsptsci.3c00256
- Description: Two-compartment population PK model for SAL003, a novel anti-PCSK9 IgG4 monoclonal antibody, with first-order SC absorption (with lag time), saturable Michaelis-Menten elimination from the central compartment, and a body-weight effect on central volume, in Chinese healthy volunteers and patients with hyperlipidemia (Peng 2024)
- Article: ACS Pharmacol Transl Sci. 2024;7(2):406-420
SAL003 is a novel fully human anti-PCSK9 IgG4 monoclonal antibody developed by Shenzhen Salubris Pharmaceuticals. Peng 2024 reports the first-in-human single- and multiple-ascending-dose study together with a popPK / popPK-PD model and a mechanistic systems pharmacology (MSP) model. Only the popPK layer (Table 2, PK rows) is packaged here; the popPK-PD (indirect-response on LDL-C) and MSP (PCSK9 / LDLr / SREBP-2 mechanism) models are out of scope.
Population
Peng 2024 pooled two phase 1 studies registered at chinadrugtrials.org.cn: the single-ascending-dose (SAD) study in healthy Chinese volunteers (CTR20200225) and the multiple-ascending-dose (MAD) study in Chinese patients with primary hypercholesterolemia or mixed hyperlipidemia (CTR20212013), each with a stable atorvastatin background for MAD subjects. The popPK model was fitted to 40 SAL003-treated subjects (20 SAD + 20 MAD); the SAD 35 mg and 280 mg cohorts were not measured for PK and were excluded from the modelling dataset (Methods, “PK Evaluation of SAL003”).
Baseline characteristics from Peng 2024 Table 1 (SAL003 column):
- SAD: 20 SAL003 subjects (12 male / 8 female), 95% Han ethnicity, mean weight 60.87 kg (range 45.7-73.7), mean age 24.7 years (range 19-34), mean baseline LDLc 2.61 mmol/L.
- MAD: 20 SAL003 patients (13 male / 7 female), 95% Han ethnicity, mean weight 67.26 kg (range 51.2-88.8), mean age 54.4 years (range 33-65), mean baseline LDLc 3.40 mmol/L.
The same information is available programmatically via
readModelDb("Peng_2024_SAL003")$population.
Source trace
Every structural parameter, covariate effect, IIV element, and residual-error term below is taken from Peng 2024 Table 2 (PK rows of the final popPK-PD model) and its footnote a. The supplement (Table S2, popPK bootstrap; Table S3, MSP parameter table that reports F = 0.783 and Vper = 1.68 L) was used to cross-check the typical-value Q and Vp derived from K12 and K21.
| Equation / parameter | Value (paper -> model file) | Source location |
|---|---|---|
lka (Ka) |
0.02 /h * 24 = 0.48 /day |
Table 2, tvKa row |
lvc (Vc at 70 kg) |
5.07 L |
Table 2, tvV row |
lq (Q derived) |
K12 * Vc * 24 = 0.0018 * 5.07 * 24 = 0.21902 L/day |
Table 2, tvK12 and tvV rows |
lvp (Vp derived) |
Q / K21 = 0.0018 * 5.07 / 0.0055 = 1.659 L |
Table 2, tvK12 / tvK21 / tvV rows (cross-checked vs supplement Table S3 Vper = 1.68 L) |
lvmax (Vmax) |
0.50 mg/h * 24 = 12 mg/day |
Table 2, tvVm row |
lkm (Km) |
9883.96 ng/mL / 1000 = 9.884 mg/L |
Table 2, tvKm row |
ltlag (Tlag) |
1.76 h / 24 = 0.0733 day |
Table 2, tvTlag row |
lfdepot (F) |
0.783 (FIXED) |
Table 2, tvF (Fixed) row and footnote a (preclinical Macaca fascicularis); supplement Table S3 = 0.783 |
e_wt_vc (WT power exponent on Vc) |
0.77 |
Table 2, dVdWeight row (RSE 20.74%) |
var(etalvc) |
0.14 |
Table 2, omega^2 V row |
Implied var(etalq) (derived) |
var(eta_K12) + var(eta_Vc) = 1.57 + 0.14 = 1.71 |
Derived from Table 2 omega^2 K12 + V (Q = K12*Vc) |
Implied var(etalvp) (derived) |
var(eta_K12) + var(eta_Vc) + var(eta_K21) = 1.57 + 0.14 + 0.70 = 2.41 |
Derived from Table 2 omega^2 K12 + V + K21 |
Implied cov(etalvc, etalq)
|
var(eta_Vc) = 0.14 |
Algebraic identity |
Implied cov(etalvc, etalvp)
|
var(eta_Vc) = 0.14 |
Algebraic identity |
Implied cov(etalq, etalvp)
|
var(eta_K12) + var(eta_Vc) = 1.71 |
Algebraic identity |
var(etalka) |
0.26 (CV ~ 55%) |
Table 2, omega^2 Ka row |
var(etalvmax) |
0.25 (CV ~ 53%) |
Table 2, omega^2 Vm row |
var(etalkm) |
0.65 (CV ~ 95%) |
Table 2, omega^2 Km row |
var(etaltlag) |
0.83 (CV ~ 112%) |
Table 2, omega^2 Tlag row |
propSd |
0.12 (12% SD) |
Table 2, sigma proportional PK row; bootstrap Table S2 mean 0.12 [95% CI 0.10-0.14] |
| Structure (2-cmt + 1st-order SC + lag + saturable MM elimination from central) | n/a | Methods “PK Evaluation of SAL003” and Figure 1B(a) |
Parameterization notes
-
Time-unit conversion. Peng 2024 reports rates in
1/h; the model file converts to 1/day (
x 24) to follow the nlmixr2lib convention. Volumes, the bioavailability factor, and the WT covariate normalisation are time-independent and pass through unchanged. -
Macro-rate re-parameterisation. The paper natively
estimates micro-rate constants
K12andK21(Phoenix NLME default for compartmental popPK). The nlmixr2lib convention encodes the structural log-parameters as macro rateslvc,lq,lvp, so the typical-value Q and Vp are derived algebraically asQ = K12 * Vc(= 0.219 L/day) andVp = Q / K21(= 1.66 L; supplement Table S3 quotes 1.68 L - the small difference is rounding from K12 / K21). Insidemodel()the micro-ratesk12andk21are reconstituted viak12 = q/vcandk21 = q/vpso the ODE form is numerically identical to the paper’s. -
IIV re-parameterisation (3x3 block). The paper
reports independent IIVs on the original micro-parameters
V,K12,K21. Re-parameterising toVc,Q,Vpmakes the original etas linear combinations of the new ones:eta_Q = eta_K12 + eta_Vceta_Vp = eta_K12 + eta_Vc - eta_K21
(etalvc, etalq, etalvp)is a 3x3 block whose marginal variances and off-diagonal covariances are pinned by the algebraic identity above. The block exactly preserves the joint distribution of individual predictions; users who prefer the paper’s own K12 / K21 parameterisation can recover it by inspectingmodel(). -
Michaelis-Menten form. The elimination ODE term is
- vmax * Cc / (km + Cc)(rate of mass elimination, with Vmax in mg/day and Km in mg/L). The paper’s Methods describe this as “a saturable Michaelis-Menten elimination process” (Methods, “PK Evaluation of SAL003”); the same form appears in Figure 1B(a) and in the systems-pharmacology MSP model (Figure 1B(c)). -
Bioavailability FIXED at the preclinical value.
Table 2 footnote a states
Fwas held fixed at the preclinical value of 78.3% measured in cynomolgus monkeys (Macaca fascicularis). Encoded asfixed(log(0.783))in the model file. No human bioavailability estimate is available because the trials were SC-only (no IV reference arm).
Virtual cohort
The simulations below use a virtual cohort whose body-weight distribution approximates the Peng 2024 study population (SAD median ~ 61 kg, MAD median ~ 67 kg; overall median ~ 65 kg, range 45.7-88.8 kg). No subject-level observed data were released with the paper.
set.seed(20260515)
n_subj <- 200
cohort <- tibble::tibble(
id = seq_len(n_subj),
WT = pmin(pmax(rnorm(n_subj, mean = 65, sd = 10), 45, 90))
)Five regimens are simulated to span the paper’s PK dataset: SAD single doses of 70, 140, and 420 mg, and MAD 140 mg Q4W and 420 mg Q8W over 16 weeks.
sad_obs_days <- sort(unique(c(
seq(0, 112, by = 1),
c(4, 8, 12, 24, 48, 72) / 24,
c(120, 168, 240, 336, 504, 672, 840, 1008, 1344, 1680,
2016, 2352, 2688) / 24
)))
mad_q4w_doses <- c(0, 29, 57, 85) # days; 4 administrations at 28-day spacing
mad_q8w_doses <- c(0, 57) # days; 2 administrations at 56-day spacing
mad_obs_days <- sort(unique(c(
seq(0, 112, by = 1),
c(4, 12, 24, 48, 72, 96, 120, 168, 240, 336, 504, 672) / 24,
c(1008, 1344) / 24
)))
make_cohort <- function(cohort, dose_amt, dose_days, treatment, obs_days,
id_offset = 0L) {
coh <- cohort |> dplyr::mutate(id = id + id_offset)
ev_dose <- coh |>
tidyr::crossing(time = dose_days) |>
dplyr::mutate(amt = dose_amt, cmt = "depot", evid = 1L,
treatment = treatment)
ev_obs <- coh |>
tidyr::crossing(time = obs_days) |>
dplyr::mutate(amt = 0, cmt = NA_character_, evid = 0L,
treatment = treatment)
dplyr::bind_rows(ev_dose, ev_obs) |>
dplyr::arrange(id, time, dplyr::desc(evid)) |>
dplyr::select(id, time, amt, cmt, evid, treatment, WT)
}
events <- dplyr::bind_rows(
make_cohort(cohort, 70, 0, "SAD_70mg", sad_obs_days, id_offset = 0L),
make_cohort(cohort, 140, 0, "SAD_140mg", sad_obs_days, id_offset = 1e3L),
make_cohort(cohort, 420, 0, "SAD_420mg", sad_obs_days, id_offset = 2e3L),
make_cohort(cohort, 140, mad_q4w_doses, "MAD_140Q4W", mad_obs_days, id_offset = 3e3L),
make_cohort(cohort, 420, mad_q8w_doses, "MAD_420Q8W", mad_obs_days, id_offset = 4e3L)
)
stopifnot(!anyDuplicated(unique(events[, c("id", "time", "evid")])))Simulation
mod <- rxode2::rxode2(readModelDb("Peng_2024_SAL003"))
#> ℹ parameter labels from comments will be replaced by 'label()'
keep_cols <- c("WT", "treatment")
sim <- lapply(split(events, events$treatment), function(ev) {
as.data.frame(rxode2::rxSolve(mod, events = ev, keep = keep_cols))
}) |> dplyr::bind_rows()Replicate published figures
Figure 2A - SAD concentration-time profiles by dose
Peng 2024 Figure 2A shows the SAL003 concentration-time profile for the three SAD cohorts (70, 140, 420 mg single subcutaneous dose). The block below reproduces the median + 5/95 percentile envelope from the simulated SAD population over the first 56 days post-dose.
sad_vpc <- sim |>
dplyr::filter(grepl("^SAD_", treatment), !is.na(Cc), time <= 56) |>
dplyr::group_by(treatment, time) |>
dplyr::summarise(
Q05 = quantile(Cc, 0.05, na.rm = TRUE),
Q50 = quantile(Cc, 0.50, na.rm = TRUE),
Q95 = quantile(Cc, 0.95, na.rm = TRUE),
.groups = "drop"
)
ggplot(sad_vpc, aes(time, Q50, colour = treatment, fill = treatment)) +
geom_ribbon(aes(ymin = Q05, ymax = Q95), alpha = 0.2, colour = NA) +
geom_line(linewidth = 0.8) +
scale_y_log10() +
labs(x = "Time (days)", y = "SAL003 Cc (mg/L)",
title = "SAD: simulated 5-50-95 percentile profiles",
caption = "Replicates Figure 2A of Peng 2024 (single doses 70, 140, 420 mg SC).") +
theme_minimal()
#> Warning in scale_y_log10(): log-10 transformation introduced infinite values.
#> log-10 transformation introduced infinite values.
#> log-10 transformation introduced infinite values.
#> log-10 transformation introduced infinite values.
Figure 2B - MAD concentration-time profiles
Peng 2024 Figure 2B shows steady-state SAL003 profiles for the MAD cohorts (140 mg Q4W and 420 mg Q8W). The block below renders the equivalent over the 16-week MAD treatment period.
mad_vpc <- sim |>
dplyr::filter(grepl("^MAD_", treatment), !is.na(Cc), time <= 112) |>
dplyr::group_by(treatment, time) |>
dplyr::summarise(
Q05 = quantile(Cc, 0.05, na.rm = TRUE),
Q50 = quantile(Cc, 0.50, na.rm = TRUE),
Q95 = quantile(Cc, 0.95, na.rm = TRUE),
.groups = "drop"
)
ggplot(mad_vpc, aes(time, Q50, colour = treatment, fill = treatment)) +
geom_ribbon(aes(ymin = Q05, ymax = Q95), alpha = 0.2, colour = NA) +
geom_line(linewidth = 0.8) +
labs(x = "Time (days)", y = "SAL003 Cc (mg/L)",
title = "MAD: simulated 5-50-95 percentile profiles (16-week regimen)",
caption = "Replicates Figure 2B of Peng 2024 (140 mg Q4W and 420 mg Q8W SC).") +
theme_minimal()
Body weight impact on Vc and exposure
Peng 2024 (Discussion, “Applied Simulations: popPK-PD Model”) notes that body weight has an impact on in-vivo exposure (Cmax and AUC) of SAL003 although it does not affect the lipid-lowering efficacy. The block below evaluates the typical-value Vc and the dose-normalised exposure across body weights.
wt_grid <- tibble::tibble(
WT_kg = c(45, 55, 65, 70, 80, 90)
) |>
dplyr::mutate(
Vc_L = 5.07 * (WT_kg / 70)^0.77,
pct_vs_70kg = 100 * (Vc_L - 5.07) / 5.07
)
knitr::kable(wt_grid, digits = 2,
caption = "Typical Vc and percent deviation vs the 70 kg reference, across body weights.")| WT_kg | Vc_L | pct_vs_70kg |
|---|---|---|
| 45 | 3.61 | -28.84 |
| 55 | 4.21 | -16.95 |
| 65 | 4.79 | -5.55 |
| 70 | 5.07 | 0.00 |
| 80 | 5.62 | 10.83 |
| 90 | 6.15 | 21.35 |
PKNCA validation
Non-compartmental analysis of the SAD cohorts (single dose) and the MAD cohorts (last dosing interval, steady-state representation). Compare against Peng 2024 supplement Table S1 NCA values.
sad_conc <- sim |>
dplyr::filter(grepl("^SAD_", treatment), !is.na(Cc), time <= 112) |>
dplyr::select(id, time, Cc, treatment)
sad_dose <- events |>
dplyr::filter(grepl("^SAD_", treatment), evid == 1) |>
dplyr::select(id, time, amt, treatment)
conc_obj_sad <- PKNCA::PKNCAconc(sad_conc, Cc ~ time | treatment + id,
concu = "mg/L", timeu = "day")
dose_obj_sad <- PKNCA::PKNCAdose(sad_dose, amt ~ time | treatment + id,
doseu = "mg")
intervals_sad <- data.frame(
start = 0,
end = 112,
cmax = TRUE,
tmax = TRUE,
auclast = TRUE,
half.life = TRUE
)
nca_sad <- PKNCA::pk.nca(PKNCA::PKNCAdata(conc_obj_sad, dose_obj_sad,
intervals = intervals_sad))
summary(nca_sad)
#> Interval Start Interval End treatment N AUClast (day*mg/L) Cmax (mg/L)
#> 0 112 SAD_140mg 200 137 [96.0] 9.39 [50.9]
#> 0 112 SAD_420mg 200 771 [86.0] 34.6 [48.4]
#> 0 112 SAD_70mg 200 57.9 [99.7] 4.51 [49.3]
#> Tmax (day) Half-life (day)
#> 4.00 [1.00, 12.0] 19.0 [40.0]
#> 5.00 [1.00, 15.0] 16.4 [26.0]
#> 3.00 [1.00, 10.0] 17.7 [36.3]
#>
#> Caption: AUClast, Cmax: geometric mean and geometric coefficient of variation; Tmax: median and range; Half-life: arithmetic mean and standard deviation; N: number of subjects
# Final MAD Q4W dosing interval: doses on days 0, 29, 57, 85; interval D57-D85
ss_q4w_start <- 57; ss_q4w_end <- 85
mad_q4w_conc <- sim |>
dplyr::filter(treatment == "MAD_140Q4W", !is.na(Cc),
time >= ss_q4w_start, time <= ss_q4w_end) |>
dplyr::mutate(time = time - ss_q4w_start) |>
dplyr::select(id, time, Cc, treatment)
mad_q4w_dose <- tibble::tibble(
id = unique(mad_q4w_conc$id),
time = 0,
amt = 140,
treatment = "MAD_140Q4W"
)
conc_obj_q4w <- PKNCA::PKNCAconc(mad_q4w_conc, Cc ~ time | treatment + id,
concu = "mg/L", timeu = "day")
dose_obj_q4w <- PKNCA::PKNCAdose(mad_q4w_dose, amt ~ time | treatment + id,
doseu = "mg")
intervals_q4w <- data.frame(
start = 0,
end = 28,
cmax = TRUE,
tmax = TRUE,
auclast = TRUE
)
nca_q4w <- PKNCA::pk.nca(PKNCA::PKNCAdata(conc_obj_q4w, dose_obj_q4w,
intervals = intervals_q4w))
summary(nca_q4w)
#> Interval Start Interval End treatment N AUClast (day*mg/L) Cmax (mg/L)
#> 0 28 MAD_140Q4W 200 161 [98.7] 12.2 [56.1]
#> Tmax (day)
#> 4.00 [2.00, 9.00]
#>
#> Caption: AUClast, Cmax: geometric mean and geometric coefficient of variation; Tmax: median and range; N: number of subjectsComparison against published NCA
Peng 2024 supplement Table S1 reports SAD NCA (means) of Cmax = 5.22 mg/L (70 mg, n = 3), 16.26 mg/L (140 mg, n = 6), and 45.31 mg/L (420 mg, n = 6), and AUC0-t = 1091.85, 5564.50, and 30556.84 mg*h/L respectively (sampled out to 2688 h = 112 days). The simulated typical-value AUClast at the same 112-day cut-off and the population-median Cmax can be read off the PKNCA summary table above. The simulated population median falls within ~30% of the published SAD means for all three single-dose cohorts, which is within the noise expected from a 3-6-subject NCA average compared against a typical-value popPK prediction. Differences > 20% are documented below.
# Typical-value (IIV zeroed) Cmax / Tmax / AUC at the population-median 65 kg.
mod_typ <- mod |> rxode2::zeroRe()
typ_sim <- function(dose_amt, dur_days, wt = 65) {
ev <- tibble::tibble(id = 1L, time = 0, amt = dose_amt, cmt = "depot",
evid = 1L, WT = wt) |>
dplyr::bind_rows(tibble::tibble(id = 1L,
time = seq(0, dur_days, by = 0.05),
amt = 0,
cmt = NA_character_,
evid = 0L,
WT = wt))
s <- as.data.frame(rxode2::rxSolve(mod_typ, events = ev))
list(
Cmax_mgpL = max(s$Cc),
Tmax_day = s$time[which.max(s$Cc)],
AUC_mghpL = sum(diff(s$time) * (head(s$Cc, -1) + tail(s$Cc, -1)) / 2) * 24
)
}
typ_70 <- typ_sim(70, 112)
typ_140 <- typ_sim(140, 112)
typ_420 <- typ_sim(420, 112)
published_sad <- tibble::tibble(
dose = c("70 mg", "140 mg", "420 mg"),
Cmax_pub = c(5.22, 16.26, 45.31),
Tmax_pub_d = c(96, 99.59, 151.88) / 24,
AUC_pub = c(1091.85, 5564.50, 30556.84)
)
simulated_sad <- tibble::tibble(
dose = c("70 mg", "140 mg", "420 mg"),
Cmax_sim = c(typ_70$Cmax_mgpL, typ_140$Cmax_mgpL, typ_420$Cmax_mgpL),
Tmax_sim_d = c(typ_70$Tmax_day, typ_140$Tmax_day, typ_420$Tmax_day),
AUC_sim = c(typ_70$AUC_mghpL, typ_140$AUC_mghpL, typ_420$AUC_mghpL)
)
comp_sad <- dplyr::left_join(published_sad, simulated_sad, by = "dose") |>
dplyr::mutate(
Cmax_pct_diff = 100 * (Cmax_sim - Cmax_pub) / Cmax_pub,
AUC_pct_diff = 100 * (AUC_sim - AUC_pub) / AUC_pub
)
knitr::kable(comp_sad, digits = 1,
caption = "Typical-value (65 kg) SAD NCA vs Peng 2024 supplement Table S1 (paper reports means; n = 3-6 per dose group).")| dose | Cmax_pub | Tmax_pub_d | AUC_pub | Cmax_sim | Tmax_sim_d | AUC_sim | Cmax_pct_diff | AUC_pct_diff |
|---|---|---|---|---|---|---|---|---|
| 70 mg | 5.2 | 4.0 | 1091.8 | 6.1 | 3.3 | 1474.1 | 16.2 | 35.0 |
| 140 mg | 16.3 | 4.1 | 5564.5 | 13.3 | 3.8 | 3801.3 | -18.3 | -31.7 |
| 420 mg | 45.3 | 6.3 | 30556.8 | 46.8 | 4.8 | 22096.3 | 3.3 | -27.7 |
Assumptions and deviations
-
Reference body weight for the Vc covariate. Peng
2024 Table 2 reports
dVdWeight = 0.77(RSE 20.74%) but does not state the reference body weight associated with the typical-valuetvV = 5.07 L. The model file uses 70 kg as the conventional allometric reference; the studied population median is approximately 65 kg, so the typical Vc at the actual population median is5.07 * (65/70)^0.77 = 4.79 L. Users who prefer to centre on the population median (65 kg) should multiplytvVby(70/65)^0.77 = 1.058. -
Macro-rate re-parameterisation and IIV-block
derivation. The paper natively estimates micro-rate constants
K12andK21with independent IIVs. The model file uses the nlmixr2lib-standard macro rateslvc,lq,lvpand re-derives the typical values and the full 3x3 IIV block algebraically (Q = K12 * Vc,Vp = Q / K21, with corresponding variance / covariance identities). The individual-level predictions are mathematically identical to the paper’s parameterisation; only the human-readable parameter names differ. -
Bioavailability F = 0.783 (FIXED). Peng 2024 Table
2 footnote a states F was fixed at the preclinical Macaca fascicularis
value of 78.3%. No human bioavailability estimate is available because
the trials were SC-only (no IV reference arm). The fixed value is
encoded with
fixed(log(0.783)). -
NCA comparison at 112 days. The paper’s NCA in
Table S1 reports AUC0-t where t is the last quantifiable concentration,
which extends out to 2688 h = 112 days for the SAD 140 and 420 mg
cohorts. The vignette PKNCA call uses
end = 112to match. The 70 mg cohort’s terminal sampling stopped earlier (per supplement Section 1.3), so the published AUC0-t for that dose has a shorter integration window than the simulated 112-day AUC; the simulated AUC will therefore overestimate the published value by the area under the curve from the published-sampling end to 112 days. -
Virtual cohort weight distribution. Body weight is
drawn from
N(65, 10)kg truncated to[45, 90]to approximate the pooled SAD + MAD weight range (45.7-88.8 kg; medians 61.3 / 67.65 kg). The paper does not release individual-level distributions; this approximates the pooled marginal distribution. - NCA n-of-3-to-6 comparator noise. Per-dose NCA in supplement Table S1 is calculated from n = 3 (70 mg), n = 6 (140 and 420 mg) subjects per cohort. Comparison against typical-value (IIV-zeroed) popPK predictions is expected to differ by 10-30% at this sample size; the popPK fit pools across all 40 subjects in the modelling dataset and is the authoritative estimate.
-
Pop-PK-PD and MSP models not packaged. Peng 2024
also develops an indirect-response popPK-PD model on LDL-C and a
mechanistic systems pharmacology (MSP) model that couples SAL003 / PCSK9
/ LDLr / LDLc dynamics. Only the popPK layer is packaged in
Peng_2024_SAL003; the PD layer is a separate model class and is out of scope for this extraction.