Every file, every method, every line explained


Table of Contents

  1. The 14 Files β€” Quick Map
  2. Data Model Files (9 files)
  3. Utility Files
  4. API Client File
  5. Core Logic β€” HotwUpscalingHelper
  6. Core Logic β€” DescaleHostRecommendationHelper
  7. How Data Flows Through the Code

1. The 14 Files β€” Quick Map

HotwHelper/
β”‚
β”œβ”€β”€ ─── DATA MODELS (blueprints for data objects) ──────────────────────
β”‚
β”œβ”€β”€ TableConstants.java              β†’ String constants for column names
β”œβ”€β”€ AtomicHOTWRequestModel.java      β†’ Input: version IDs for atomic HOTW
β”œβ”€β”€ CreateHotwRunDetailsInput.java   β†’ Input: create/update a HOTW run
β”œβ”€β”€ HOTWHelperModel.java             β†’ Output: result of running HOTW
β”œβ”€β”€ HotwAllDetails.java              β†’ Data: fleet details + order summary
β”œβ”€β”€ HotwExecutionAllDetailsInput.java β†’ Full execution record with ASG/SPCO data
β”œβ”€β”€ OrderDetails.java                β†’ Per-fleet order summary (for table)
β”œβ”€β”€ PreferredASG.java                β†’ Database record: preferred ASGs per fleet
β”œβ”€β”€ DescaleRecommendationWithClb.java β†’ Per-ASG descale recommendation + lower bound
β”‚
β”œβ”€β”€ ─── UTILITIES (helper logic) ───────────────────────────────────────
β”‚
β”œβ”€β”€ TableUtil.java                   β†’ Build markdown tables for SIM tickets
β”œβ”€β”€ HardwareOrdersUtil.java          β†’ Build HOTW details, calculate orders, FMC
β”‚
β”œβ”€β”€ ─── API CLIENT ─────────────────────────────────────────────────────
β”‚
β”œβ”€β”€ EPICBackendHotwApiCallsCommon.java β†’ Call EPIC backend HOTW REST APIs
β”‚
β”œβ”€β”€ ─── CORE LOGIC (main workflow) ─────────────────────────────────────
β”‚
β”œβ”€β”€ HotwUpscalingHelper.java         β†’ MAIN: upscaling workflow orchestrator
└── DescaleHostRecommendationHelper.java β†’ Post-event descaling recommendations

2. Data Model Files

2.1 TableConstants.java

Purpose: Defines constant column names used when building markdown tables.

public final class TableConstants {
    //  final class = cannot be subclassed

    public static final String FLEET_ID = "Fleet Id";
    public static final String PEAK_TPM = "Peak TPM";
    public static final String BAU_TPM = "BAU TPM";
    public static final String REQUIRED_HOSTS = "Required Hosts";
    public static final String HOSTS_PRESENT_IN_APOLLO = "Hosts Present in Apollo";
    public static final String HOSTS_ORDERED_BY_EPIC = "Hosts Ordered By EPIC";
    public static final String PENDING_HOSTS_ORDERED_BY_EPIC = "Pending Hosts Ordered By EPIC";
    public static final String CT_Peak_Factor = "CT Peak Factor";
    public static final String Buffer_PERCENTAGE = "Buffer%";
    public static final String FMC_ORDER_DETAILS = "FMC Order Details";
    public static final String SPCO_VALUE = "Capacity Override Value";
    public static final String DASH_SEPARATOR = "---";
    public static final String REASON_PENDING_MILESTONES = "Reason for milestones pending";
    public static final String ACTION_ITEMS = "Action Items";
    public static final String FLEET = "Fleet";
    public static final String MILESTONE = "Milestone";
    public static final String ETA = "ETA";
    public static final String ACTIONS = "Pending Actions";
}

Why this pattern?

// BAD β€” "magic strings" scattered in code:
tableBuilder.add("Fleet Id");   // typo risk, hard to change everywhere
tableBuilder.add("Fleet Id");   // duplication β€” must change in 10 places!

// GOOD β€” central constant:
tableBuilder.add(TableConstants.FLEET_ID);   // if name changes β†’ fix ONE place!

Usage in other files:

// In TableUtil.java:
headerJoiner.add(TableConstants.FLEET_ID)
            .add(TableConstants.PEAK_TPM)
            .add(TableConstants.BAU_TPM);

// In HardwareOrdersUtil.java:
totalOrderHardwareAndSpcoMap.put(TableConstants.SPCO_VALUE, spcoValue);
totalOrderHardwareAndSpcoMap.put(TableConstants.PENDING_HOSTS_ORDERED_BY_EPIC, pendingOrders);

2.2 AtomicHOTWRequestModel.java

Purpose: Carries version IDs for an β€œAtomic HOTW” β€” a HOTW run triggered when a fleet’s version changes.

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@Jacksonized
@Builder
public class AtomicHOTWRequestModel {
    
    @JsonProperty("PreviousVersionId")
    private String previousVersionId;    // fleet version BEFORE the change
    
    @JsonProperty("CurrentVersionId")
    private String currentVersionId;     // fleet version AFTER the change
}

What is β€œAtomic HOTW”?

  • Normal HOTW: runs weekly on all fleets
  • Atomic HOTW: triggered when a specific fleet’s data changes (version update)
  • Purpose: immediately re-check if hardware ordering is needed for just that fleet
  • It compares previousVersionId vs currentVersionId to calculate the delta (difference) in required hosts

Used in:

// HotwHandler.java (the Lambda trigger β€” outside this package):
AtomicHOTWRequestModel request = JsonUtil.parseJson(event, AtomicHOTWRequestModel.class);
String previousVersionId = request.getPreviousVersionId();
String currentVersionId = request.getCurrentVersionId();

// Then calls HotwUpscalingHelper.handleAtomic():
hotwUpscalingHelper.handleAtomic(
    fleetId, eventId, runId,
    previousVersionId,   // ← from the model
    currentVersionId     // ← from the model
);

2.3 CreateHotwRunDetailsInput.java

Purpose: Data to create or update a HOTW β€œrun” record in the database.

@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@Jacksonized
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CreateHotwRunDetailsInput {
    
    @JsonProperty("RunId")
    int runId;           // unique ID for this weekly HOTW execution run
    
    @JsonProperty("RunStatus")
    String runStatus;    // "InProgress", "Completed", "Failed"
    
    @JsonProperty("EventId")
    String eventId;      // which event this run is for (e.g., "PrimeDay26")
    
    @JsonProperty("OrderType")
    String orderType;    // "Standard" or "Emergent"
}

Usage:

// At the end of getResult() in HotwUpscalingHelper.java:
epicBackendHotwApiCallsCommon.updateHotwRunDetails(
    CreateHotwRunDetailsInput.builder()
        .eventId(eventId)
        .runId(Integer.parseInt(runId))
        .orderType(
            StringUtils.isNotBlank(sev2TicketId) ? "Emergent" : "Standard"
            //  if sev2 ticket was created β†’ Emergent, otherwise Standard
        )
        .build()
);

What is β€œRunId”?

  • Every time HOTW runs (e.g., weekly), it gets a new RunId
  • All execution details for that run are grouped by RunId
  • Lets you see history: β€œRunId 5 on March 15 processed 200 fleets”

2.4 HOTWHelperModel.java

Purpose: The result returned by the HOTW helper after processing one fleet. Contains everything needed for notifications.

@Data
@Jacksonized
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class HOTWHelperModel {
    
    private Integer hostNeeded;           // how many additional hosts to order
    private Service service;              // the service object (with email, owner, etc.)
    private String hardwareOrderSimLink;  // link to the SIM ticket for tracking
    private boolean isDifferentialOrder;  // is this an update to an existing order?
    private Fleet fleet;                  // the fleet object (with all fleet data)
    private EmergentDetails emergentDetails;  // extra info if this is an emergency order
    
    // ────────────────────────────────────────
    // INNER CLASS: EmergentDetails
    // Only populated when it's an emergent (close to peak) order
    // ────────────────────────────────────────
    @Data
    @Builder
    @Jacksonized
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class EmergentDetails {
    
        @JsonProperty("IsEmergent")
        Boolean isEmergent;           // is this order past the emergent date?
        
        @JsonProperty("EventName")
        String eventName;             // "Prime Day 2026" (human readable)
        
        @JsonProperty("DaysToPeakGameDays")
        Integer daysToPeakGameDays;   // how many days until peak starts
        
        @JsonProperty("IsEmergentCloseToPeak")
        Boolean isEmergentCloseToPeak;  // very close to peak? (< X days)
    }
}

The static keyword on inner class:

public static class EmergentDetails { ... }
//     ↑ static inner class

// Static = you can create EmergentDetails WITHOUT first creating HOTWHelperModel:
HOTWHelperModel.EmergentDetails details = HOTWHelperModel.EmergentDetails.builder()
    .isEmergent(true)
    .eventName("Prime Day 2026")
    .build();

// Non-static inner class would require: (we don't use this pattern here)
// HOTWHelperModel outerModel = new HOTWHelperModel();
// outerModel.new EmergentDetails();  // awkward!

How it’s built:

// In HotwUpscalingHelper.getResult():
HOTWHelperModel.EmergentDetails emergentDetails =
    HOTWHelperModel.EmergentDetails.builder()
        .isEmergent(StringUtils.isNotBlank(sev2TicketId))
        //           ↑ if sev2TicketId is not blank β†’ it IS emergent
        .eventName(event.getEventName())
        .build();

// Then set remaining fields:
setEmergentDetailsRemainingParameters(emergentDetails, event);
// (sets daysToPeakGameDays and isEmergentCloseToPeak)

HOTWHelperModel result = HOTWHelperModel.builder()
    .hostNeeded(hostNeeded)
    .service(service)
    .hardwareOrderSimLink(hardwareOrderSimLink)
    .isDifferentialOrder(isDifferentialOrder)
    .fleet(fleet)
    .emergentDetails(emergentDetails)
    .build();

2.5 HotwAllDetails.java

Purpose: Aggregated details for one fleet’s HOTW run β€” TPM numbers, host counts, order summaries.

@Data @Jacksonized @Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class HotwAllDetails {
    
    @JsonProperty("FleetDetails")
    private FleetDetails fleetDetails;        // numerical fleet data
    
    @JsonProperty("totalHardwareOrdered")
    private String totalHardwareOrdered;      // total hosts ordered so far
    
    @JsonProperty("fmcOrderDetails")
    private String fmcOrderDetails;           // FMC link (clickable in SIM)
    
    @JsonProperty("spcoValue")
    private String spcoValue;                 // capacity override value
    
    @JsonProperty("pendingHardwareOrdered")
    private String pendingHardwareOrdered;    // orders not yet fulfilled
    
    // ────────────────────────────────────────
    // INNER CLASS: FleetDetails
    // The numerical data for a fleet
    // ────────────────────────────────────────
    @Data @Builder
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public static class FleetDetails {
    
        @JsonProperty("fleetId")
        private String fleetId;           // "[FleetA-NA](https://...)" β€” with link!
        
        @JsonProperty("peakTpm")
        private int peakTpm;              // projected peak TPM
        
        @JsonProperty("bufferFactor")
        private double bufferFactor;      // buffer multiplier used
        
        @JsonProperty("ctFactor")
        private double ctFactor;          // CloudTune peak factor
        
        @JsonProperty("bauTpm")
        private int bauTpm;               // BAU (non-peak) TPM
        
        @JsonProperty("hostRequired")
        private int hostRequired;         // total hosts needed for peak
        
        @JsonProperty("maxHostsInApollo")
        private int maxHostsInApollo;     // current hosts in Apollo (already running)
        
        @JsonProperty("hostThroughPutTPM")
        private int hostThroughPutTPM;    // TPM capacity per single host
    }
}

The key formula here:

BAU TPM = Peak TPM Γ· (CT Peak Factor Γ— Buffer Factor)

Example:
  peakTpm = 10,000
  ctFactor = 2.0  (CloudTune says peak is 2x BAU)
  bufferFactor = 1.3  (30% safety buffer)
  bauTpm = 10,000 Γ· (2.0 Γ— 1.3) = 3,846

Required hosts = peakTpm Γ· hostThroughputTPM Γ— AZ Factor
  = 10,000 Γ· 570 Γ— 1.125 = ~20 hosts

2.6 HotwExecutionAllDetailsInput.java

Purpose: The MOST COMPLEX model in HotwHelper. Stores everything that happened during one fleet’s HOTW execution β€” for audit trail and the HOTW dashboard.

(Already explained in Layer 3. See the full annotation breakdown there.)

Key nested classes summary:

Inner Class Stores
HotwExecutionDetails TPM numbers, hosts needed, hosts in Apollo, SPCO sum
CapacityOverrideDetails One SPCO (Capacity Override) placed
FulfillmentDetail Hardware fulfillment status from AWS
StatusOfHOTW enum SUCCESS / FAIL / PARTIAL_SUCCESS

2.7 OrderDetails.java

Purpose: Simple per-fleet order summary β€” all fields are Strings for easy table/CSV formatting.

@Data @Jacksonized @Builder
public class OrderDetails {
    @JsonProperty("fleetId")     private String fleetId;
    @JsonProperty("peakTpm")     private String peakTpm;
    @JsonProperty("bauTpm")      private String bauTpm;
    @JsonProperty("ctFactor")    private String ctFactor;
    @JsonProperty("bufferFactor") private String bufferFactor;
    @JsonProperty("hostRequired") private String hostRequired;
    @JsonProperty("maxHostsInApollo") private String maxHostsInApollo;
    @JsonProperty("fmcOrderDetails") private String fmcOrderDetails;
    @JsonProperty("spcoValue")   private String spcoValue;
    @JsonProperty("pendingHardwareOrdered") private String pendingHardwareOrdered;
}

Note: All fields are String even though TPM is normally a number. Why?

  • Makes it easy to put in a table (no conversion needed)
  • Allows values like β€œN/A” when data isn’t available
  • Used when building markdown tables for SIM ticket descriptions

2.8 PreferredASG.java

Purpose: Represents a database record of which ASGs (Auto Scaling Groups) are preferred for a fleet. Used for intelligent host distribution.

@JsonIgnoreProperties(ignoreUnknown = true)
@Data @Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
@Jacksonized
public class PreferredASG {

    @JsonProperty("RecordId")
    private int recordId;           // database primary key
    
    @JsonProperty("FleetIndexId")
    private int fleetIndexId;       // links to fleet (DB ID)
    
    @JsonProperty("EventIndexId")
    private int eventIndexId;       // links to event (DB ID)
    
    @JsonProperty("ASGS")
    private String asgs;            // JSON string: ["asg-1", "asg-2"]
    
    @JsonProperty("UpdatedBy")
    private String updatedBy;       // who set this preference
    
    @JsonProperty("UpdatedAt")
    private Timestamp updatedAt;    // when it was set
}

Why β€œpreferred” ASGs?

A fleet may have many ASGs (Auto Scaling Groups) across different availability zones. Engineers can mark which ASGs to prefer for ordering β€” e.g., β€œorder from the US-EAST-1a ASG first because it has better hardware available.”

How it’s used in HotwUpscalingHelper:

// Get the list of preferred ASGs (most recent first):
List<PreferredASG> preferredASGList =
    epicHotwApiCallsCommon.getPreferredASGList(fleetId, eventId);

if (preferredASGList.size() > 1) {
    // Compare latest preferred ASG set vs previous preferred ASG set
    String preferredASG = preferredASGList.get(0).getAsgs();         // latest
    String penUltimatePreferredASG = preferredASGList.get(1).getAsgs(); // second latest
    
    // Find ASGs that were in previous set but NOT in latest set
    // These are "decommissioned" ASGs β€” set them to BAU host count
    List<String> diffASGList = new ArrayList<>(penUltimatePreferredASGList);
    diffASGList.removeAll(latestPreferredASGList);
    // diffASGList = ASGs removed from preference (should go back to BAU level)
}

2.9 DescaleRecommendationWithClb.java

Purpose: Carries descale recommendation + lower bound for one ASG.

@Data @Jacksonized @Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class DescaleRecommendationWithClb {

    @JsonProperty("AsgCapacityLowerBound")
    Integer asgCapacityLowerBound;
    // ↑ Minimum hosts this ASG must always have (even after descaling)
    // Cannot go below this! Safety floor.
    
    @JsonProperty("AsgDescaleRecommendationHost")
    Integer asgDescaleRecommendationHost;
    // ↑ Recommended target host count after descaling
    // Will be β‰₯ asgCapacityLowerBound
}

Example:

Current hosts: 200
Required descale: remove 50 hosts
AsgCapacityLowerBound: 120

Recommended host count = 200 - 50 = 150
Since 150 > 120 (lower bound) β†’ safe to descale to 150 βœ…

If: Required descale: remove 100 hosts
   Recommended = 200 - 100 = 100
   But 100 < 120 (lower bound) β†’ cannot do this descale! ❌
   β†’ Report as failure

3. Utility Files

3.1 TableUtil.java β€” Building Markdown Tables

Purpose: Creates properly-formatted markdown tables for SIM ticket descriptions.

What is a markdown table?

| Fleet Id | Peak TPM | BAU TPM | Required Hosts |
| --- | --- | --- | --- |
| [FleetA-NA](https://epic-link) | 10000 | 4807 | 200 |
|    |    |    |    |

This renders as a formatted table in SIM (Amazon’s ticket system).

Method 1: getDescriptionHeader(String eventId)

public static StringBuilder getDescriptionHeader(String eventId) {
    // Creates the main SIM ticket description with a table header
    
    String header = "We have created this SIM for " + eventId + " hardware orders..."
    + " Here is a [link](...) to EPIC ...\n\n";
    
    // Returns: header text + table column headers + separator row
}

Method 2: getCommentHeader(String fleetId, String eventId)

public static StringBuilder getCommentHeader(String fleetId, String eventId) {
    // Used when adding a COMMENT to an existing SIM (update to existing ticket)
    String header = "Here is the updated SPCO and FMC order details for **FleetA-NA**:\n\n";
    // Returns: header text + table column headers + separator row
}

Method 3: getTableBuilder(String header, String eventId) (private)

private static StringBuilder getTableBuilder(String header, String eventId) {
    StringBuilder tableBuilder = new StringBuilder();
    tableBuilder.append(header).append("\n");
    
    // Build column headers using StringJoiner:
    StringJoiner headerJoiner = new StringJoiner(" | ", "| ", " |");
    // prefix="| "  separator=" | "  suffix=" |"
    
    headerJoiner
        .add(TableConstants.FLEET_ID)         // "Fleet Id"
        .add(TableConstants.PEAK_TPM)         // "Peak TPM"
        .add(TableConstants.BAU_TPM)          // "BAU TPM"
        .add(TableConstants.CT_Peak_Factor)   // "CT Peak Factor"
        .add(TableConstants.Buffer_PERCENTAGE)// "Buffer%"
        .add(TableConstants.REQUIRED_HOSTS)   // "Required Hosts"
        .add(TableConstants.HOSTS_PRESENT_IN_APOLLO)  // "Hosts Present in Apollo"
        .add(TableConstants.SPCO_VALUE);      // "Capacity Override Value"
    
    // HVE events (special events) have fewer columns:
    if (!CommonUtil.isHveEvent(eventId)) {
        headerJoiner
            .add(TableConstants.PENDING_HOSTS_ORDERED_BY_EPIC)
            .add(TableConstants.FMC_ORDER_DETAILS);
        columnCount = 11;
    }
    
    // Build separator row: | --- | --- | --- | ...
    StringJoiner separatorJoiner = new StringJoiner(" | ", "| ", " |");
    for (int i = 0; i < columnCount; i++) {
        separatorJoiner.add(TableConstants.DASH_SEPARATOR);  // "---"
    }
    
    tableBuilder.append(headerJoiner.toString()).append("\n");
    tableBuilder.append(separatorJoiner.toString());
    return tableBuilder;
}

Result of getTableBuilder:

| Fleet Id | Peak TPM | BAU TPM | CT Peak Factor | Buffer% | Required Hosts | Hosts Present in Apollo | Capacity Override Value | Pending Hosts Ordered By EPIC | FMC Order Details |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |

Method 4: descriptionFormatter(StringBuilder, HotwAllDetails, String)

// Adds ONE DATA ROW to the table:
public static void descriptionFormatter(
    StringBuilder tableBuilder,  // table being built
    HotwAllDetails details,      // fleet data
    String eventId) {
    
    // Build the row:
    String row = String.join(" | ",
        details.getFleetDetails().getFleetId(),          // "[FleetA-NA](link)"
        String.valueOf(details.getFleetDetails().getPeakTpm()),     // "10000"
        String.valueOf(details.getFleetDetails().getBauTpm()),      // "4807"
        String.valueOf(details.getFleetDetails().getCtFactor()),    // "2.08"
        String.valueOf(details.getFleetDetails().getBufferFactor()), // "1.3"
        String.valueOf(details.getFleetDetails().getHostRequired()), // "200"
        String.valueOf(details.getFleetDetails().getMaxHostsInApollo()), // "180"
        details.getSpcoValue()                           // "20"
    );
    
    if (!CommonUtil.isHveEvent(eventId)) {
        row = String.join(" | ", row,
            details.getPendingHardwareOrdered(),  // "5"
            fmcOrderDetails                       // "[FMC LINK](url)"
        );
    }
    
    tableBuilder.append("| ").append(row).append(" |\n");
    // adds empty row for spacing:
    tableBuilder.append("| ").append("    | ".repeat(columnCount - 1)).append("   |\n");
}

Result β€” the full table with data:

| [FleetA-NA](https://...) | 10000 | 4807 | 2.08 | 1.3 | 200 | 180 | 20 | 5 | [FMC LINK](url) |
|    |    |    |    |    |    |    |    |    |    |

Method 5: getFmcLink(...) β€” build FMC URL

public static String getFmcLink(
    String fleetApolloName,   // "FORTRESSService/NA/X1/Prod"
    String regionName,        // "us-east-1"
    Map<String, List<String>> hardwareOrderTags) {  // event-specific tags
    
    List<String> taglist = hardwareOrderTags.get(REGIONS_NAME_MAPPING.get(regionName));
    
    Map<String, List<String>> queryParams = new HashMap<>();
    queryParams.put("tab", List.of("ec2"));                    // tab=ec2
    queryParams.put("tags[]", taglist);                         // tags[]=PrimeDay26_NA
    queryParams.put("environment[]", List.of(fleetApolloName)); // environment[]=FORTRESSService/NA/X1/Prod
    
    return generateDynamicURL(EndpointConstants.FMC_URL, queryParams);
    // Result: https://fmc.amazon.com/?tab=ec2&tags[]=PrimeDay26_NA&environment[]=FORTRESSService/NA/X1/Prod
}

Method 6: generateDynamicURL(...) β€” build URL with query params

public static String generateDynamicURL(
    String baseUrl,
    Map<String, List<String>> queryParams) {
    
    // Convert Map to URL query string using streams:
    String queryString = queryParams.entrySet()
        .stream()
        .flatMap(entry -> {
            String key = entry.getKey();
            List<String> values = entry.getValue();
            // For each value: create "key=value" string
            return values.stream().map(value -> key + "=" + value);
        })
        .collect(Collectors.joining("&"));
    //                               ↑ join all with "&"
    
    URI uri = new URI(baseUrl + "?" + queryString);
    // Result: "https://fmc.url?tab=ec2&tags[]=tagA&environment[]=apolloName"
    
    return uri.toString();
}

3.2 HardwareOrdersUtil.java β€” Hardware Order Calculations

Purpose: The computational workhorse β€” calculates hardware order numbers, builds HOTW details, interacts with ScalingPlanner.

Key Design β€” Singleton-like Factory:

@AllArgsConstructor
public class HardwareOrdersUtil {
    private LambdaLogger logger;
    private static Map<String, ScalingPlannerApiServiceProxy> scalingPlannerApiServiceProxyByRegion;
    //              ↑ static β€” shared across all instances
    
    // Creates one proxy per AWS region (us-east-1, eu-west-1, etc.)
    private static void initialize(LambdaLogger logger) {
        scalingPlannerApiServiceProxyByRegion = new HashMap<>();
        scalingPlannerApiServiceProxyByRegion.put(US_EAST_1,
            LambdaEnvironment.getScalingPlannerApiServiceProxy(logger, US_EAST_1));
        scalingPlannerApiServiceProxyByRegion.put(EU_WEST_1,
            LambdaEnvironment.getScalingPlannerApiServiceProxy(logger, EU_WEST_1));
        // ... more regions ...
    }
    
    // Factory method β€” always initializes before returning instance:
    public static HardwareOrdersUtil getInstance(LambdaLogger logger) {
        initialize(logger);             // set up region proxies
        return new HardwareOrdersUtil(logger);
    }
}

Method: getFleetDetails(Fleet, Event, Service)

public HotwAllDetails.FleetDetails getFleetDetails(Fleet fleet, Event event, Service service) {
    FleetProjectionItem projection = fleet.getFleetProjection()[0];
    FleetConfiguration config = fleet.getFleetConfiguration();
    
    // Step 1: Get total peak TPM
    int totalInputTpm = projection.getInputTPM() != null ? projection.getInputTPM() : 0;
    
    // Step 2: Get buffer factor used
    double bufferUsed = getBufferUsed(projection);
    // getBufferUsed reads "BufferUsed" from the projection's TPM properties
    
    // Step 3: Get CT (CloudTune) peak factor for this region
    double ctFactor = CommonUtil.getPreferredCTPFForService(
        service,
        REGIONS_NAME_MAPPING.get(fleet.getFleetConfiguration().getRegion()),
        event
    );
    
    // Step 4: Calculate BAU TPM (reverse from peak)
    double bauTpm = calculateBauTpm(totalInputTpm, ctFactor, bufferUsed);
    // bauTpm = peakTpm / (ctFactor * bufferUsed)
    
    // Step 5: Get projected hosts (already calculated by EPIC)
    double projectedHosts = projection.getProjectedHosts();
    double hostThroughputTPM = config.getHostThroughputTPM();
    double maxHostCount = config.getMaxHostCount() != null ? config.getMaxHostCount() : 0.0;
    
    // Step 6: Build and return FleetDetails object
    return HotwAllDetails.FleetDetails.builder()
        .peakTpm(totalInputTpm)
        .bauTpm((int) bauTpm)
        .bufferFactor(bufferUsed)
        .ctFactor(ctFactor)
        .maxHostsInApollo((int) maxHostCount)
        .hostRequired((int) projectedHosts)
        .hostThroughPutTPM((int) hostThroughputTPM)
        .build();
}

Method: buildHOTWDetails(Fleet, Event, Service, ...)

This is the BIG method that aggregates all data for one fleet:

public HotwAllDetails buildHOTWDetails(
    Fleet fleet, Event event, Service service,
    ScalingPlannerHelper scalingPlannerHelper,
    Map<String, List<String>> hardwareOrderTags,
    HotwExecutionAllDetailsInput hotwExecutionAllDetailsInput) {
    
    // 1. Get numerical fleet details (TPM, hosts):
    HotwAllDetails.FleetDetails fleetDetails = getFleetDetails(fleet, event, service);
    
    // 2. Build HotwAllDetails with just fleet details initially:
    HotwAllDetails hotwAllDetails = HotwAllDetails.builder().fleetDetails(fleetDetails).build();
    
    // 3. Set the fleetId as a clickable markdown link:
    String fleetLink = buildFleetLink(event.getEventId(), service.getServiceId(), fleet.getFleetId());
    hotwAllDetails.getFleetDetails().setFleetId("[" + fleetId + "](" + fleetLink + ")");
    // Result: "[FleetA-NA](https://console.harmony.a2z.com/epic/Event/PrimeDay26/...)"
    
    // 4. Build the FMC link:
    String fmcLink = getFmcLink(fleet, hardwareOrderTags);
    
    // 5. Query ScalingPlanner for total hardware ordered and SPCO values:
    Map<String, Integer> totalOrderMap = getTotalHardwareOrderForEvent(
        scalingPlannerHelper, fleet.getFleetId(), event.getEventId(),
        asgList, scalingPlannerProxy, hotwExecutionAllDetailsInput
    );
    
    // 6. Set the remaining fields:
    hotwAllDetails.setFmcOrderDetails("[FMC LINK](" + fmcLink + ")");
    hotwAllDetails.setSpcoValue(String.valueOf(totalOrderMap.get(TableConstants.SPCO_VALUE)));
    hotwAllDetails.setPendingHardwareOrdered(
        String.valueOf(totalOrderMap.get(TableConstants.PENDING_HOSTS_ORDERED_BY_EPIC)));
    
    // 7. Update the audit record:
    updateHotwExecutionAllDetails(hotwExecutionAllDetailsInput, ...);
    
    return hotwAllDetails;
}

Method: getTotalHardwareOrderForEvent(...)

Queries ScalingPlanner API to count how many hosts have been ordered:

// For each ASG in the fleet:
for (AutoScalingGroupDataModel asg : asgList) {
    
    // Get ScalingPlanner info for this ASG:
    AsgScalingPlannerInfo asgInfo = scalingPlannerHelper.getAsgInfoResponse(fleetId, eventId, asg.getAsgTag());
    
    // Build the capacity override name: "asgName_PrimeDay26"
    String capacityOverrideName = extractAsgName(asgInfo.getAsgInfo().getArn()) + "_" + eventId;
    
    // Fetch all capacity overrides and their fulfillment details:
    List<CapacityOverrideWithFulfillmentDetails> fulfillmentDetails =
        getCapacityOverrideWithFulfillmentDetails(proxy, asgInfo, capacityOverrideName);
    
    for (CapacityOverrideWithFulfillmentDetails override : fulfillmentDetails) {
        spcoValue += override.getCapacityOverride().getOverrideValue();  // add to SPCO total
        
        for (CapacityOverrideFulfillmentDetails fulfillment : override.getFulfillmentDetails()) {
            totalHardwareOrdered += fulfillment.getFulfillmentValue();   // actual hosts ordered
            pendingOrders += calculatePendingHardwareOrder(fulfillment); // pending orders
        }
    }
}

Method: calculatePendingHardwareOrder()

private int calculatePendingHardwareOrder(
    CapacityOverrideFulfillmentDetails fulfillment) {
    
    // Is the order still pending (not yet approved/fulfilled)?
    if (PENDING.equals(fulfillment.getFulfillmentStatus())
        || PENDING_BUSINESS_APPROVAL.equals(fulfillment.getFulfillmentStatus())
        || PENDING_FINANCIAL_APPROVAL.equals(fulfillment.getFulfillmentStatus())
        || PENDING_REGIONAL_APPROVAL.equals(fulfillment.getFulfillmentStatus())) {
        
        return fulfillment.getFulfillmentValue();  // count these hosts as "pending"
    }
    return 0;  // not pending (completed or cancelled)
}

4. API Client File

EPICBackendHotwApiCallsCommon.java

Purpose: Makes HTTP API calls to the EPIC backend for HOTW-related operations.

Pattern used β€” Private Constructor + Static Factory:

@AllArgsConstructor(access = AccessLevel.PRIVATE)  // private constructor
public class EPICBackendHotwApiCallsCommon {
    private final EPICBackendApiProxy epicBackendApiProxy;  // HTTP client
    private final LambdaLogger logger;
    
    // Public factory method:
    public static EPICBackendHotwApiCallsCommon getInstance(
        EPICBackendApiProxy proxy, LambdaLogger logger) {
        return new EPICBackendHotwApiCallsCommon(proxy, logger);
    }
}

API call methods:

Method HTTP Path Purpose
createOrUpdateHotwRunDetails() POST /hotw/runDetail Create HOTW run record
updateHotwRunDetails() PUT /hotw/runDetail Update run status
createHotwExecutionDetails() POST /hotw/executionDetail Save execution details
createHotwDashboardWithAsgDetails() POST /hotw/dashboardDetail Save dashboard data
getAsgDetails() GET /hotw/asgDetail/{fleetId} Get ASG details
getHOTWExecutionHistoryForFleetId() GET /hotw/executionDetail/{eventId}/{fleetId} Get history
getPreferredASGList() GET /hotw/asgsPreference/{eventId}/{fleetId} Get preferred ASGs

How each method works:

public void createHotwExecutionDetails(
    HotwExecutionAllDetailsInput input) {
    try {
        // 1. Convert Java object to JSON string:
        String jsonInput = JsonUtil.toJson(input);
        
        logger.log("Create Input Json: " + jsonInput);
        
        // 2. Call EPIC backend API:
        String response = this.epicBackendApiProxy
            .apigPostCall("/hotw/executionDetail", jsonInput);
        //                ↑ path              ↑ JSON body
        
        logger.log("Response: " + response);
        
    } catch (Exception e) {
        e.printStackTrace();
        logger.log("Unable to create Hotw execution details entry");
        throw new IllegalStateException("The erroneous response received", e);
        //         ↑ wrap original exception and re-throw
    }
}

Special method: getLatestHOTWExecutionDetailsForEventFleet()

// This one uses a SQL QUERY via the RDS execute endpoint!
public List<HotwExecutionDetailsWithRunId> getLatestHOTWExecutionDetailsForEventFleet(
    Integer fleetIndexId, Integer eventIndexId) {
    
    // Build the SQL query:
    String sql = QueryUtil.queryForFetchingLastHOTWExecutionDetailsForEventFleet(
        fleetIndexId, eventIndexId);
    // SQL looks like: SELECT * FROM hotw_execution WHERE fleet_id = ? AND event_id = ?
    
    // Wrap it in a request object:
    ExecuteQueryRequest queryRequest = ExecuteQueryRequest.builder()
        .query(sql)
        .build();
    
    // Call the executeQuery endpoint (which runs SQL on RDS):
    String response = this.epicBackendApiProxy
        .apigPutCall("/rds/executeQuery", JsonUtil.toJson(queryRequest));
    
    // Deserialize response to list of objects:
    return JsonUtil.parseJson(response,
        new TypeReference<List<HotwExecutionDetailsWithRunId>>() {});
}

5. Core Logic β€” HotwUpscalingHelper.java

This is the most complex file β€” the main orchestrator for HOTW upscaling.

Constructor β€” Setting Up Dependencies

public HotwUpscalingHelper(Context context) {
    // Step 1: Get the logger (for CloudWatch logs)
    this.logger = context.getLogger();
    
    // Step 2: Initialize environment (reads Lambda env variables)
    LambdaEnvironment.initialize();
    stage = LambdaEnvironment.getSTAGE();   // "beta", "gamma", "prod"
    region = LambdaEnvironment.getREGION(); // "us-east-1"
    
    // Step 3: Create API proxy clients
    this.epicBackendApiProxy = LambdaEnvironment.getEpicBackendApiProxy(context);
    this.hotwApiProxy = LambdaEnvironment.getHOTWApiProxy(context);
    this.fmcApiServiceProxy = LambdaEnvironment.getFMCApiServiceProxy(logger, stage);
    
    // Step 4: Create business logic helpers (all are factory-method style)
    this.epicBackendFleetApiCallsCommon = 
        EPICBackendFleetApiCallsCommon.getInstance(epicBackendApiProxy, logger);
    this.epicBackendApiCallsCommon =
        EPICBackendApiCallsCommon.getInstance(epicBackendApiProxy, logger);
    this.milestoneCommons = new MilestoneCommons(logger);
    this.epicBackendApiCallsMilestone =
        EPICBackendApiCallsMilestone.getInstance(epicBackendApiProxy, logger);
    this.recommendDistribution = new MinChangeWeightsDistribution();
    this.scalingPlannerHelper = ScalingPlannerHelper.getInstance(
        logger, epicBackendApiCallsCommon, recommendDistribution);
    this.fmcHelper = FMCHelper.getInstance(logger, epicBackendApiProxy, epicBackendApiCallsCommon);
    this.apolloHelper = ApolloHelper.getInstance(logger, epicBackendApiProxy, epicBackendApiCallsCommon);
    this.epicBackendServiceApiCallsCommon =
        EPICBackendServiceApiCallsCommon.getInstance(epicBackendApiProxy, logger);
    this.epicBackendTicketApiCallsCommon =
        EPICBackendTicketApiCallsCommon.getInstance(epicBackendApiProxy, logger);
    
    // Step 5: Create AWS SDK clients
    this.maxisAPIServiceProxy = LambdaEnvironment.getMaxisApiServiceProxy(logger);
    this.ticketyAPIServiceProxy = LambdaEnvironment.getTicketyAPIServiceProxy(logger);
    this.hardwareOrdersUtil = HardwareOrdersUtil.getInstance(logger);
    this.sns = AmazonSNSClientBuilder.defaultClient();
    this.sqs = AmazonSQSClientBuilder.defaultClient();
    
    // Step 6: Create HOTW-specific API client
    this.epicBackendHotwApiCallsCommon =
        EPICBackendHotwApiCallsCommon.getInstance(epicBackendApiProxy, logger);
    this.epicHotwApiCallsCommon =
        EPICBackendHotwApiCallsCommon.getInstance(hotwApiProxy, logger);
    //  ↑ Note: uses hotwApiProxy (different base URL) β€” for reading preferred ASGs
    
    // Step 7: Sleep strategy and SQS queue
    this.sleepStrategy = LambdaEnvironment.getSleepStrategy();
    this.queueUrl = sqs.getQueueUrl(APOLLO_QUEUE_NAME).getQueueUrl();
}

Public Method 1: handle() β€” Normal Weekly HOTW

public void handle(String fleetId, String eventId, String runId) {
    logger.log("HOTW upscaling workflow invoked for fleetId: " + fleetId);
    try {
        // Step 1: Get latest fleet data
        Fleet fleet = epicBackendFleetApiCallsCommon.callGetLatestEventFleet(fleetId, eventId);
        
        // Step 2: Validate fleet data is complete
        validateFleetData(fleet);
        
        // Step 3: Refresh FMC and Apollo data
        updateFMCAndApolloDetails(fleetId, eventId);
        
        // Step 4: Wait for Apollo data to update
        this.sleepStrategy.sleep(5000);   // sleep 5 seconds
        
        // Step 5: Run the main workflow (calculate + order)
        HOTWHelperModel result = getResult(fleetId, eventId, runId, null);
        
        // Step 6: Send SNS notification (only if hosts are needed and not differential)
        if (!result.isDifferentialOrder() && result.getHostNeeded() > 0) {
            publishHardwareDetailsToSNS(
                result.getService(), fleetId, eventId, result.getHardwareOrderSimLink()
            );
        }
        
    } catch (Exception e) {
        logger.log("Error in HOTW workflow for FleetId:" + fleetId + " Error:" + e);
        e.printStackTrace();
    }
}

Public Method 2: handleAtomic() β€” Triggered by Fleet Version Change

public void handleAtomic(String fleetId, String eventId, String runId,
    String previousVersionId, String currentVersionId) {
    try {
        // Step 1: Get the event β€” skip if it's BAU (not a peak event)
        Event event = epicBackendApiCallsCommon.callGetEvent(eventId);
        if (EVENT_TYPE_BAU.equals(event.getEventType())) {
            logger.log("Skipping HOTW for BAU event");
            return;   // ← early return, skip everything else
        }
        
        // Step 2: Get BOTH versions of the fleet
        Fleet previousFleet = epicBackendFleetApiCallsCommon
            .callGetFleetObjectWithVersion(fleetId, previousVersionId);
        Fleet currentFleet = epicBackendFleetApiCallsCommon
            .callGetFleetObjectWithVersion(fleetId, currentVersionId);
        
        // Step 3: Calculate the DELTA (change in required hosts)
        int deltaRequiredHost =
            currentFleet.getFleetProjection()[0].getProjectedHosts().intValue()
            - previousFleet.getFleetProjection()[0].getProjectedHosts().intValue();
        // deltaRequiredHost = current - previous
        // Positive β†’ need more hosts
        // Negative β†’ need fewer hosts
        // Zero β†’ no change
        
        // Step 4: Validate current fleet data
        validateFleetData(currentFleet);
        
        // Step 5: Update FMC and Apollo data
        updateFMCAndApolloDetails(fleetId, eventId);
        this.sleepStrategy.sleep(5000);
        
        // Step 6: Run main workflow
        HOTWHelperModel result = getResult(fleetId, eventId, runId, currentVersionId);
        
        // Step 7: Only notify if there was an actual change in hosts needed
        if (deltaRequiredHost != 0) {
            publishAtomicDetailsToSNS(
                result.getService(), fleetId, eventId,
                previousVersionId, currentVersionId,
                result.getHardwareOrderSimLink(),
                result.getEmergentDetails()
            );
        }
        
    } catch (Exception e) {
        logger.log("Error in atomic HOTW for FleetId:" + fleetId);
        e.printStackTrace();
    }
}

Private Method: getResult() β€” The Core Logic

This is the heart of HOTW. Let me walk through it step by step:

private HOTWHelperModel getResult(String fleetId, String eventId, String runId, String versionId)
    throws Exception {
    try {
        // ══════ STEP 1: GET FLEET DATA ══════
        Fleet fleet = epicBackendFleetApiCallsCommon.callGetLatestEventFleet(fleetId, eventId);
        Fleet versionfleet = null;
        int projectedHost;
        String user = AUTO;  // "AUTO" = HOTW ran automatically
        
        if (versionId != null) {
            // Atomic HOTW: use specific version
            versionfleet = epicBackendFleetApiCallsCommon
                .callGetFleetObjectWithVersion(fleetId, versionId);
            projectedHost = versionfleet.getFleetProjection()[0].getProjectedHosts().intValue();
            user = versionfleet.getAuditMetadata().getUser();  // who made the change
        } else {
            // Normal HOTW: use latest projection
            projectedHost = fleet.getFleetProjection()[0].getProjectedHosts().intValue();
        }
        
        // ══════ STEP 2: CLASSIFY HOST TYPES ══════
        // Find what type of hosts the fleet uses (e.g., r5.xlarge, c5.2xlarge)
        Map<String, ArrayList<String>> hostTypeWithAsg = new HashMap<>();
        // hostTypeWithAsg = { "r5.xlarge": ["asg-1a", "asg-1b"], "c5.2xlarge": ["asg-2a"] }
        
        Map<String, Integer> sortedMap = CommonUtil.getHostType(fleet, hostTypeWithAsg);
        // sortedMap = { "r5.xlarge": 150, "c5.2xlarge": 30 }  (sorted by count, desc)
        
        // Get the MOST COMMON host type:
        Map.Entry<String, Integer> maxEntry = sortedMap.entrySet().iterator().next();
        String hostType = maxEntry.getKey();   // "r5.xlarge" β€” the dominant host type
        
        // Get the ASG list for the dominant host type:
        List<String> asgListOfRequiredHostType = hostTypeWithAsg.get(hostType);
        // ["asg-1a", "asg-1b"]
        
        // ══════ STEP 3: CALCULATE HOSTS NEEDED ══════
        Integer maxHostInApollo = fleet.getFleetConfiguration().getMaxHostCount().intValue();
        // Current hosts running in Apollo (e.g., 180)
        
        // Get pending host orders (already ordered but not delivered):
        Optional<HostOrderStatus> hostOrderStatusOpt =
            Arrays.stream(fleet.getHostOrderStatuses()).findFirst();
        Integer totalPendingHost =
            getPendingApprovalHostOrders(hostOrderStatusOpt.get().getHostsPendingApproval())
            + hostOrderStatusOpt.get().getHostsPendingDelivery();
        // totalPendingHost = pending approval + pending delivery (e.g., 10)
        
        // KEY FORMULA: how many more hosts do we need to order?
        Integer hostNeeded = projectedHost - (maxHostInApollo + totalPendingHost);
        // Example: 200 projected - (180 in Apollo + 10 pending) = 10 more needed
        
        // Safety check β€” don't order 1000+ at once:
        if (hostNeeded >= 1000) {
            throw new IllegalStateException("More than 1000 hosts ordering for:" + fleetId);
        }
        
        // Including pending orders in total recommendation:
        Integer hostNeededWithPending = hostNeeded
            + getPendingApprovalHostOrders(hostOrderStatusOpt.get().getHostsPendingApproval())
            + hostOrderStatusOpt.get().getHostsPendingDelivery();
        
        // ══════ STEP 4: GET PER-ASG RECOMMENDATIONS ══════
        // Ask ScalingPlanner: "how to distribute hostNeededWithPending across ASGs?"
        Map<String, Integer> asgToHostRecommendationMap =
            scalingPlannerHelper.getRecommendedHostForEachAsg(
                fleetId, eventId, hostType, hostNeededWithPending, asgListOfRequiredHostType);
        // Result: { "asg-1a": 8, "asg-1b": 12 }  (distribute 20 hosts across 2 ASGs)
        
        // ══════ STEP 5: HANDLE PREFERRED ASG CHANGES ══════
        // Check if preferred ASGs changed recently (some ASGs removed from preference):
        List<PreferredASG> preferredASGList = epicHotwApiCallsCommon.getPreferredASGList(fleetId, eventId);
        
        if (preferredASGList.size() > 1) {
            // Compare latest vs previous preferred ASG list
            // Removed ASGs should go back to BAU host count
            // New/preferred ASGs get the new recommendations
            // (complex logic to merge these two maps)
            // ... (see full code for details) ...
        }
        
        // ══════ STEP 6: HANDLE HOST TYPE MIGRATION ══════
        // Some fleets are migrating from old host type to new host type:
        if (FLEET_TO_PREFERRED_HOST_TYPE_MAP.containsKey(fleetId)) {
            // Old host type: keep at BAU count (remove extra from old type)
            // New host type: give all the recommendations
            // (merge both maps)
        }
        
        // ══════ STEP 7: CHECK EMERGENT ORDERING ══════
        // Is today past the emergent scaling start date?
        Boolean isStuckInPendingApproval = fmcHelper.getIfFMCOrderNotCompletedInAsgList(...);
        
        String sev2TicketId = checkAndCreateSev2TicketIfNeeded(
            event,
            checkIfDeltaHostsPositive(hostNeeded, totalPendingHost),
            isStuckInPendingApproval
        );
        // sev2TicketId = "" if not emergent, or actual ticket ID if emergent
        
        // ══════ STEP 8: PLACE CAPACITY OVERRIDES (SPCO) ══════
        String hardwareOrderSimLink = fetchArnUpdateSPCOAndReturnSIMLink(
            fleet, event, service, asgToHostRecommendationMap, ...
        );
        // This places the actual SPCO orders in AWS ScalingPlanner
        // Returns the SIM ticket link for tracking
        
        // ══════ STEP 9: UPDATE FMC ORDERS ══════
        fmcHelper.updateFMCorders(fleetId, eventId, fmcApiServiceProxy,
            StringUtils.isNotBlank(sev2TicketId));  // isEmergent flag
        
        // ══════ STEP 10: UPDATE SIM TICKET ══════
        boolean isDifferentialOrder = updateHardwareOrderSimDetails(
            service, fleet, hardwareOrderSimLink, hostNeeded, totalPeakFmcOrders, event, ...
        );
        
        // ══════ STEP 11: SAVE AUDIT RECORDS ══════
        epicBackendHotwApiCallsCommon.createHotwExecutionDetails(hotwExecutionAllDetailsInput);
        epicBackendHotwApiCallsCommon.updateHotwRunDetails(
            CreateHotwRunDetailsInput.builder()
                .eventId(eventId)
                .runId(Integer.parseInt(runId))
                .orderType(StringUtils.isNotBlank(sev2TicketId) ? "Emergent" : "Standard")
                .build()
        );
        
        // ══════ BUILD AND RETURN RESULT ══════
        HOTWHelperModel.EmergentDetails emergentDetails = HOTWHelperModel.EmergentDetails.builder()
            .isEmergent(StringUtils.isNotBlank(sev2TicketId))
            .eventName(event.getEventName())
            .build();
        setEmergentDetailsRemainingParameters(emergentDetails, event);
        
        return HOTWHelperModel.builder()
            .hostNeeded(hostNeeded)
            .service(service)
            .hardwareOrderSimLink(hardwareOrderSimLink)
            .isDifferentialOrder(isDifferentialOrder)
            .fleet(fleet)
            .emergentDetails(emergentDetails)
            .build();
        
    } catch (Exception e) {
        e.printStackTrace();
        throw e;  // re-throw so caller knows it failed
        
    } finally {
        // ══════ ALWAYS RUNS (even if exception occurred) ══════
        // Push to Apollo queue (refreshes Apollo config):
        SendMessageRequest apolloMessage = ApolloHelper.getMessageRequestForApolloQueue(
            fleetId, eventId, queueUrl, 600  // 600 second delay
        );
        sqs.sendMessage(apolloMessage);
        
        // Trigger milestone check (mark HardwareOrder milestone as done):
        this.publishToMilestoneSNSTopic(fleetId, eventId);
    }
}

Private Method: validateFleetData()

private void validateFleetData(Fleet fleet) {
    // NULL CHECKS β€” exit if any critical data is missing:
    if (fleet == null                                               // no fleet at all?
        || fleet.getFleetProjection() == null                      // no projection?
        || fleet.getFleetProjection().length == 0                  // empty array?
        || fleet.getFleetProjection()[0] == null                   // null first element?
        || fleet.getFleetConfiguration() == null                   // no config?
        || fleet.getFleetConfiguration().getHostThroughputTPM() <= 1  // invalid TPM?
        || fleet.getFleetProjection()[0].getInputTPM() <= 5        // near-zero traffic?
        || fleet.getFleetProjection()[0].getProjectedHosts() == null  // no host projection?
        || fleet.getFleetProjection()[0].getTotalInputTPMProperties() == null) {
        
        throw new IllegalArgumentException(
            "Invalid fleet data, fix fleet details for fleetID: " + fleet.getFleetId()
        );
    }
    
    // BUSINESS RULES:
    if (!REGISTERED.equals(fleet.getFleetType())) {
        // Only "Registered" fleets can have hardware orders
        throw new IllegalStateException("FleetType is not Registered for: " + fleet.getFleetId());
    }
    
    // Customer must have verified the traffic link:
    if (!Boolean.TRUE.equals(
        fleet.getFleetProjection()[0].getTotalInputTPMProperties()
            .getTotalInputTPMLinkVerifiedByCustomer())) {
        throw new IllegalStateException("TotalInputTPMLink not verified by customer");
    }
}

This places the actual SPCO (hardware orders) in AWS ScalingPlanner:

// For each ASG and its recommended host count:
for (Map.Entry<String, Integer> entry : asgToHostRecommendationMap.entrySet()) {
    String asgTag = entry.getKey();        // "asg-1a"
    Integer hostForThisAsg = entry.getValue(); // 8 (hosts to order for this ASG)
    
    // Skip if 0 hosts needed:
    if (hostForThisAsg == 0) continue;
    
    // Check lower bound β€” can't go below minimum:
    int asgCapacityLowerBound = asgScalingPlannerInfo.getAsgCapacityLowerBound();
    if (hostForThisAsg < asgCapacityLowerBound) {
        // Can't place order below lower bound!
        isLowerBoundIssue = true;
        continue;
    }
    
    // Build the scaling strategy (gradual vs immediate):
    ScalingStrategy scalingStrategy = ScalingStrategyFactory.getScalingStrategy(...);
    List<CapacityOverrideDataModel> overrides = scalingStrategy.getCapacityOverrideDataModels();
    
    // Build SPCO request:
    PutCapacityOverrideInput spcoRequest = PutCapacityOverrideInput.builder()
        .arn(asgScalingPlannerInfo.getAsgInfo().getArn())  // AWS ARN of the ASG
        .justification(hardwareOrderSimLink)                // SIM ticket link
        .overrides(overrides)                              // scaling plan
        .placeOrderIfRequired("true")                      // yes, order hardware!
        .requester("EPIC")                                 // who is requesting
        .build();
    
    // Send to ScalingPlanner API:
    String response = scalingPlannerHelper.getScalingPlanOverrideV2ResultResponse(spcoRequest);
    logger.log("SPCO response for ASG " + asgTag + ": " + response);
}

Private Method: checkAndCreateSev2TicketIfNeeded()

private String checkAndCreateSev2TicketIfNeeded(
    Event event, Boolean isDeltaHostsPositive, Boolean isStuckInPendingApproval) {
    
    // Only create emergent ticket if past the emergent start date:
    if (checkIfCurrentDayPastEmergentScaleStartDate(event)) {
        
        if (isDeltaHostsPositive || isStuckInPendingApproval) {
            // Check if a sev2 ticket already exists:
            List<Ticket> existingTickets = epicBackendTicketApiCallsCommon.callGetOpenTicket(
                event.getEventId(), EPIC_SERVICE, EPIC_SERVICE, EMERGENT_ORDERING_PURPOSE
            );
            String ticketId = existingTickets.isEmpty() ? "" : existingTickets.get(0).getTicketId();
            
            if (StringUtils.isBlank(ticketId)) {
                // No existing ticket β†’ create new sev2:
                return createSev2PendingTicketOnEPICCTI(event);
            } else {
                return ticketId;  // use existing ticket
            }
        }
    }
    return "";  // not emergent β†’ empty string
}

6. Core Logic β€” DescaleHostRecommendationHelper.java

Purpose: After a peak event ends, recommends which hosts to remove (descale) per ASG.

handle(String fleetId, String eventId)

public Map<String, DescaleRecommendationWithClb> handle(String fleetId, String eventId) {
    // Returns: { "asg-1a": {recommendedHosts: 80, lowerBound: 50} }
    
    Map<String, Integer> asgToDescaleMap = new HashMap<>();
    Map<String, DescaleRecommendationWithClb> result = new HashMap<>();
    
    try {
        // Step 1: Refresh Apollo data
        Integer maxHostInApollo =
            apolloHelper.fetchAndUpdateApolloDetails(apolloLambdaFunction, fleetId, eventId);
        
        // Step 2: Get fleet
        Fleet fleet = epicBackendFleetApiCallsCommon.callGetLatestEventFleet(fleetId, eventId);
        
        // Step 3: Get required descale hosts (how many to remove)
        Optional<FleetProjectionItem> projectionOpt =
            Arrays.stream(fleet.getFleetProjection()).findFirst();
        Integer requiredDescaleHosts = projectionOpt.get().getRequiredDescaleHosts();
        
        // Step 4: Validate
        if (requiredDescaleHosts == null || requiredDescaleHosts == 0) {
            return result;  // nothing to descale
        }
        if ((maxHostInApollo - requiredDescaleHosts) < 0) {
            return result;  // can't descale more than we have
        }
        
        // Step 5: Calculate per-ASG descale recommendation
        // Target: maxHostInApollo - requiredDescaleHosts
        // Example: 200 current - 50 to remove = 150 target
        asgToDescaleMap = descaleHostRecommendation(fleet, maxHostInApollo - requiredDescaleHosts);
        
        // Step 6: Get CLB (Capacity Lower Bound) values from EPIC backend
        List<EAPInputDetails.ASGDetail> eapDetails =
            epicBackendHotwApiCallsCommon.getAsgDetails(fleetId);
        
        // Step 7: For each ASG, combine recommendation + lower bound:
        for (EAPInputDetails.ASGDetail asgDetail : eapDetails) {
            String asgTag = asgDetail.getAsgTag();
            int clb = asgDetail.getAsgCapacityLowerBound();
            
            if (asgToDescaleMap.containsKey(asgTag)) {
                int recommendation = asgToDescaleMap.get(asgTag);
                result.put(asgTag, DescaleRecommendationWithClb.builder()
                    .asgDescaleRecommendationHost(recommendation)
                    .asgCapacityLowerBound(clb)
                    .build());
            }
        }
        
        return result;
        
    } catch (Exception e) {
        logger.log("Error in descale recommendation for:" + fleetId);
        e.printStackTrace();
        return result;  // return empty map β€” don't crash the caller
    }
}

descaleHostRecommendation(Fleet, Integer) β€” The Algorithm

private Map<String, Integer> descaleHostRecommendation(Fleet fleet, Integer targetHostCount) {
    // targetHostCount = maxHostsInApollo - hostsToRemove
    // We need to distribute these remaining hosts across ASGs
    
    Map<String, Integer> asgWithCount = new HashMap<>();
    Map<String, Integer> asgToDescale = new HashMap<>();
    
    // Start with current host counts per ASG:
    for (AutoScalingGroupDataModel asg : fleet.getFleetConfiguration().getAsgProperties()) {
        asgWithCount.put(asg.getAsgTag(), asg.getHostCount());
    }
    // asgWithCount = { "asg-1a": 100, "asg-1b": 60, "asg-1c": 40 }
    
    int hostsToRemove = getTotalHosts(asgWithCount) - targetHostCount;
    // Remove hosts one by one from the largest ASG each time
    // (greedy algorithm: always pick the largest to keep things balanced)
    
    if (hostsToRemove > 0) {
        while (hostsToRemove > 0) {
            // Find the ASG with the most hosts:
            String maxKey = "";
            int maxValue = 0;
            for (Map.Entry<String, Integer> entry : asgWithCount.entrySet()) {
                if (entry.getValue() > maxValue) {
                    maxValue = entry.getValue();
                    maxKey = entry.getKey();
                }
            }
            
            if (maxValue == 0) break;  // no more hosts to remove
            
            // Remove one host from largest ASG:
            asgWithCount.put(maxKey, asgWithCount.get(maxKey) - 1);
            hostsToRemove--;
            asgToDescale.put(maxKey, asgWithCount.get(maxKey));
        }
    }
    
    return asgToDescale;
    // Result: { "asg-1a": 75, "asg-1b": 45 }  (recommended counts after descale)
}

Example walkthrough:

Before: asg-1a=100, asg-1b=60, asg-1c=40  (Total=200)
Target: 150 hosts (remove 50)

Round 1: Remove from asg-1a (largest 100) β†’ asg-1a=99
Round 2: Remove from asg-1a (still largest 99) β†’ asg-1a=98
...
Round 41: Remove from asg-1a (60=asg-1b) β†’ now asg-1a=60, asg-1b=60
Round 42: Remove from asg-1a (tie, picks first alphabetically) β†’ asg-1a=59
...eventually balances to: asg-1a=67, asg-1b=50, asg-1c=33  (Total=150)

7. How Data Flows Through the Code

HotwHandler.java (Lambda entry point β€” outside HotwHelper)
    β”‚
    β”‚  Creates HotwUpscalingHelper(context)
    β–Ό
HotwUpscalingHelper.handle(fleetId, eventId, runId)
    β”‚
    β”œβ”€β”€β”€ validateFleetData(fleet)
    β”‚       checks nulls, registered type, customer verification
    β”‚
    β”œβ”€β”€β”€ updateFMCAndApolloDetails(fleetId, eventId)
    β”‚       └── fmcHelper.updateFMCorders(...)
    β”‚       └── apolloHelper.fetchAndUpdateApolloDetails(...)
    β”‚
    β”œβ”€β”€β”€ sleep(5000ms)  [wait for Apollo update to propagate]
    β”‚
    └─── getResult(fleetId, eventId, runId, null)
            β”‚
            β”œβ”€β”€ Get fleet + calculate projectedHost
            β”œβ”€β”€ CommonUtil.getHostType() β†’ sortedMap + hostTypeWithAsg
            β”œβ”€β”€ Calculate hostNeeded = projected - (inApollo + pending)
            β”œβ”€β”€ scalingPlannerHelper.getRecommendedHostForEachAsg()
            β”‚       β†’ asgToHostRecommendationMap
            β”œβ”€β”€ Handle PreferredASG changes (if any)
            β”œβ”€β”€ Handle host type migration (if any)
            β”œβ”€β”€ checkAndCreateSev2TicketIfNeeded()
            β”‚       β†’ sev2TicketId (empty or actual ticket ID)
            β”œβ”€β”€ fetchArnUpdateSPCOAndReturnSIMLink()
            β”‚       β”œβ”€β”€ For each ASG: check EAP status
            β”‚       β”œβ”€β”€ If all EAP enabled:
            β”‚       β”‚       β”œβ”€β”€ getSIMLink() β†’ hardware order SIM link
            β”‚       β”‚       └── For each ASG: place PutCapacityOverride (SPCO)
            β”‚       └── Return simLink
            β”œβ”€β”€ fmcHelper.updateFMCorders()
            β”œβ”€β”€ updateHardwareOrderSimDetails()
            β”‚       β”œβ”€β”€ If fleet already in SIM: add comment with updated data
            β”‚       └── If fleet new to SIM: update SIM description + create ticket entry
            β”œβ”€β”€ createHotwExecutionDetails() β†’ save audit record
            β”œβ”€β”€ updateHotwRunDetails() β†’ update run record
            β”œβ”€β”€ Build HOTWHelperModel and return
            β”‚
            └── [FINALLY ALWAYS RUNS]:
                    β”œβ”€β”€ sqs.sendMessage β†’ Apollo queue (trigger Apollo refresh)
                    └── publishToMilestoneSNSTopic β†’ trigger milestone check

Back in handle():
    └── publishHardwareDetailsToSNS() β†’ notify service owner via SNS/email

β†’ Continue to Layer 5: Full System Flow