Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
691 changes: 691 additions & 0 deletions exercise-1302-async-nexus/exercise-1302-README.md

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions exercise-1302-async-nexus/exercise/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>io.temporal.warmups</groupId>
<artifactId>exercise-1302-async-nexus-java</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>Exercise 1302 - Temporal Async Nexus (Java)</name>
<description>Upgrade from sync to async Nexus: one new concept, infinite new possibilities</description>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<temporal.version>1.27.0</temporal.version>
</properties>

<dependencies>
<!-- Temporal SDK -->
<dependency>
<groupId>io.temporal</groupId>
<artifactId>temporal-sdk</artifactId>
<version>${temporal.version}</version>
</dependency>

<!-- SLF4J for logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>

<!-- Exec plugin: 6 execution targets -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<!-- Default: mvn compile exec:java — Checkpoint 0: sync timeout demo -->
<mainClass>exercise.PaymentProcessingService</mainClass>
</configuration>
<executions>
<!-- Checkpoint 1: mvn compile exec:java@activity-test -->
<execution>
<id>activity-test</id>
<configuration>
<mainClass>compliance.temporal.InvestigationActivityTest</mainClass>
</configuration>
</execution>
<!-- Checkpoint 2: Terminal 3: mvn compile exec:java@compliance-worker -->
<execution>
<id>compliance-worker</id>
<configuration>
<mainClass>compliance.temporal.ComplianceWorkerApp</mainClass>
</configuration>
</execution>
<!-- Checkpoint 2: Terminal 4: mvn compile exec:java@payments-worker -->
<execution>
<id>payments-worker</id>
<configuration>
<mainClass>payments.temporal.PaymentsWorkerApp</mainClass>
</configuration>
</execution>
<!-- Checkpoint 3: Terminal 5: mvn compile exec:java@starter -->
<execution>
<id>starter</id>
<configuration>
<mainClass>payments.temporal.PaymentStarter</mainClass>
</configuration>
</execution>
<!-- Checkpoint 4 (Heist Test): mvn compile exec:java@starter-rerun -->
<execution>
<id>starter-rerun</id>
<configuration>
<mainClass>payments.temporal.PaymentStarterRerun</mainClass>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package compliance;

import compliance.domain.InvestigationRequest;
import compliance.domain.InvestigationResult;
import io.temporal.activity.Activity;

/**
* [GIVEN] The 3-phase forensic investigation logic.
*
* Phase 1: OFAC Screening — checks sanctioned entities and countries
* Phase 2: Pattern Analysis — detects structuring, layering, unusual routing
* Phase 3: Final Review — senior analyst sign-off with risk scoring
*
* Investigation durations per transaction:
* TXN-ALPHA ($3,500 US→Germany): ~15 seconds total
* TXN-BETA ($47,500 US→Caymans): ~35 seconds total
* TXN-GAMMA ($180,000 US→Iran): ~60 seconds total
*
* Students use this class as-is — focus is on wiring it into Temporal activities,
* not the investigation logic itself.
*/
public class ComplianceInvestigator {

public InvestigationResult runFullInvestigation(InvestigationRequest request) {
String txn = request.getTransactionId();
String prefix = "[INV-" + txn.replace("TXN-", "") + "]";

// Determine investigation profile based on transaction
int[] phaseDurations = getInvestigationProfile(request);

try {
// Phase 1: OFAC Screening
System.out.println(prefix + " Phase 1/3: OFAC screening — checking sanctioned entities...");
heartbeat("Phase 1/3: OFAC screening");
sleep(phaseDurations[0]);
System.out.println(prefix + " Phase 1/3: OFAC screening complete.");

// Phase 2: Pattern Analysis
System.out.println(prefix + " Phase 2/3: Pattern analysis — detecting structuring and layering...");
heartbeat("Phase 2/3: Pattern analysis");
sleep(phaseDurations[1]);
System.out.println(prefix + " Phase 2/3: Pattern analysis complete.");

// Phase 3: Final Review
System.out.println(prefix + " Phase 3/3: Final review — senior analyst sign-off...");
heartbeat("Phase 3/3: Final review");
sleep(phaseDurations[2]);
System.out.println(prefix + " Phase 3/3: Final review complete.");

} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Investigation interrupted for " + txn, e);
}

return buildResult(request);
}

private int[] getInvestigationProfile(InvestigationRequest request) {
double amount = request.getAmount();
String dest = request.getReceiverCountry();

// TXN-GAMMA: sanctioned country — full 60-second deep investigation
if (dest.equalsIgnoreCase("Iran") || dest.equalsIgnoreCase("North Korea")
|| dest.equalsIgnoreCase("Cuba") || dest.equalsIgnoreCase("Syria")) {
return new int[]{15000, 20000, 25000}; // 60s total
}
// TXN-BETA: large international — 35-second investigation
if (amount >= 10000 && !request.getSenderCountry().equalsIgnoreCase(dest)) {
return new int[]{10000, 15000, 10000}; // 35s total
}
// TXN-ALPHA: routine — 15-second investigation
return new int[]{5000, 5000, 5000}; // 15s total
}

private InvestigationResult buildResult(InvestigationRequest request) {
String dest = request.getReceiverCountry();
double amount = request.getAmount();

// Sanctioned country — always blocked
if (dest.equalsIgnoreCase("Iran") || dest.equalsIgnoreCase("North Korea")
|| dest.equalsIgnoreCase("Cuba") || dest.equalsIgnoreCase("Syria")) {
return new InvestigationResult(request.getTransactionId(), true, "CRITICAL",
"Transaction blocked: destination country is OFAC-sanctioned. "
+ "All 3 investigation phases confirm: payment to " + dest + " violates regulatory requirements.");
}

// Large international — medium risk, approved
if (amount >= 10000) {
return new InvestigationResult(request.getTransactionId(), false, "MEDIUM",
"Transaction approved with elevated monitoring. Amount $"
+ String.format("%.0f", amount) + " to " + dest
+ " triggers enhanced due diligence. No structuring patterns detected.");
}

// Routine — low risk, approved
return new InvestigationResult(request.getTransactionId(), false, "LOW",
"Transaction approved. Routine transfer to " + dest
+ ". All 3 investigation phases passed. No flags raised.");
}

private void heartbeat(String phase) {
try {
// Heartbeat so Temporal knows the activity is still alive during long phases
Activity.getExecutionContext().heartbeat(phase);
} catch (Exception e) {
// Not running inside a Temporal activity (e.g., test runner) — ignore
}
}

private void sleep(int millis) throws InterruptedException {
Thread.sleep(millis);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package compliance;

import compliance.domain.InvestigationRequest;
import compliance.domain.InvestigationResult;

/**
* [GIVEN] A mock compliance agent used ONLY in Checkpoint 0.
*
* This simulates what happened before the async Nexus upgrade:
* the compliance team's new 3-phase investigation pipeline takes 30+ seconds,
* but the Payments team's sync Nexus call has a 5-second timeout.
*
* Result: every transaction times out. The Payments team pages on-call.
*
* This class is NOT used in your actual implementation — it exists to
* demonstrate the problem that async Nexus solves.
*
* Run Checkpoint 0 to see the timeout:
* mvn compile exec:java
*/
public class SlowComplianceAgent {

public InvestigationResult runFullInvestigation(InvestigationRequest request) {
System.out.println("[SlowComplianceAgent] Starting compliance pipeline for "
+ request.getTransactionId());
System.out.println("[SlowComplianceAgent] Phase 1/3: OFAC screening (this takes a while...)");

try {
Thread.sleep(10000); // Phase 1: 10 seconds
System.out.println("[SlowComplianceAgent] Phase 2/3: Pattern analysis...");
Thread.sleep(10000); // Phase 2: 10 seconds
System.out.println("[SlowComplianceAgent] Phase 3/3: Final review...");
Thread.sleep(10000); // Phase 3: 10 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

// We never get here in practice — the caller times out first
return new InvestigationResult(request.getTransactionId(), false, "LOW",
"Investigation complete (but caller already gave up)");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package compliance.domain;

/**
* [GIVEN] Input to a compliance investigation.
* Sent by the Payments team via Nexus to the Compliance team.
*/
public class InvestigationRequest {
public String transactionId;
public double amount;
public String senderCountry;
public String receiverCountry;
public String description;

public InvestigationRequest() {}

public InvestigationRequest(String transactionId, double amount,
String senderCountry, String receiverCountry,
String description) {
this.transactionId = transactionId;
this.amount = amount;
this.senderCountry = senderCountry;
this.receiverCountry = receiverCountry;
this.description = description;
}

public String getTransactionId() { return transactionId; }
public double getAmount() { return amount; }
public String getSenderCountry() { return senderCountry; }
public String getReceiverCountry() { return receiverCountry; }
public String getDescription() { return description; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package compliance.domain;

/**
* [GIVEN] Result of a 3-phase compliance investigation.
* Returned by the Compliance team to the Payments team via async Nexus.
*
* Fields:
* blocked — true = block the payment, false = allow it
* riskLevel — "LOW", "MEDIUM", or "CRITICAL"
* summary — one-line summary of all 3 investigation phases
*/
public class InvestigationResult {
public String transactionId;
public boolean blocked;
public String riskLevel; // "LOW", "MEDIUM", "CRITICAL"
public String summary;

public InvestigationResult() {}

public InvestigationResult(String transactionId, boolean blocked,
String riskLevel, String summary) {
this.transactionId = transactionId;
this.blocked = blocked;
this.riskLevel = riskLevel;
this.summary = summary;
}

public String getTransactionId() { return transactionId; }
public boolean isBlocked() { return blocked; }
public String getRiskLevel() { return riskLevel; }
public String getSummary() { return summary; }

@Override
public String toString() {
return String.format("InvestigationResult{txn=%s, blocked=%s, risk=%s, summary='%s'}",
transactionId, blocked, riskLevel, summary);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package compliance.temporal;

import compliance.domain.InvestigationRequest;
import compliance.domain.InvestigationResult;
import io.temporal.workflow.WorkflowInterface;
import io.temporal.workflow.WorkflowMethod;

/**
* [GIVEN] Workflow interface for the compliance investigation workflow.
* Implemented by you in ComplianceInvestigationWorkflowImpl.java.
*
* This workflow runs on the Compliance team's worker. It orchestrates
* the 3-phase investigation via ComplianceInvestigationActivity.
*
* IMPORTANT: The method name here must exactly match what you use in
* ComplianceNexusServiceImpl when building the fromWorkflowMethod() handler.
*
* client.newWorkflowStub(ComplianceInvestigationWorkflow.class, ...)::investigate
* ^^^^^^^^^^^
* must match this method name
*/
@WorkflowInterface
public interface ComplianceInvestigationWorkflow {
@WorkflowMethod
InvestigationResult investigate(InvestigationRequest request);
}
Loading