Core Services & Components
The Business Logic That Actually Runs Everything
🎯 What You'll Learn
- How Dione's core services actually work
- Real business logic implementation patterns
- Service integration points and their quirks
- Common debugging scenarios and solutions
- Performance considerations and bottlenecks
Service Architecture Overview
⚠️ Important Context
Dione's services are not microservices. They're SOAP-based web services within a monolithic application. This affects how you think about service boundaries, transactions, and debugging.
Core Service Categories
Authentication & Session
Login, logout, session management, and user context
Customer Management
Customer onboarding, KYC, profile management
Payment Processing
Payment initiation, validation, and processing
FX Trading
Currency exchange, rate management, trade execution
Open Banking
Bank account integration, transaction retrieval
Reporting & Analytics
Transaction reporting, compliance data
Service Location Map
services/
├── auth/ # Authentication & Session Management
│ ├── AuthServiceIMPL.java
│ ├── LoginAndProfileHandlerImpl.java
│ └── SessionManager.java
├── customer/ # Customer Management
│ ├── CustomerManagementIMPL.java
│ ├── CustomerOnboardingHandler.java
│ └── KYCProcessor.java
├── pfx/ # Payment & FX Services
│ ├── payment/
│ │ ├── PaymentManagementIMPL.java
│ │ └── PaymentValidationHandler.java
│ ├── fx/
│ │ ├── FXTradingIMPL.java
│ │ └── RateManagementHandler.java
│ └── openbanking/
│ ├── OpenBankingIMPL.java
│ └── BankAccountHandler.java
└── reporting/ # Reporting Services
├── TransactionReportIMPL.java
└── ComplianceReportHandler.java
Authentication Services
The authentication system is custom-built and handles multi-tenant login, session management, and user context.
Login Flow Implementation
@WebService(serviceName = "WSAuth",
targetNamespace = "http://auth.services.business.ngop.gtg.currenciesdirect.com/")
public class AuthServiceIMPL implements WSAuth {
@Autowired
private LoginAndProfileHandlerImpl loginAndProfileHandlerRef;
@Override
public AuthResponse login(Holder<ResponseStatusHeader> responseStatusHeader,
AuthRequest authRequest, String locale, String source) {
try {
// Validate organization code
if (!isValidOrganization(authRequest.getOrgCode())) {
throw new NGOPBaseException("Invalid organization code");
}
// Create session data
PojoMap sessionData = loginAndProfileHandlerRef.createNGOPSession(
responseStatusHeader, authRequest, locale, source);
// Build response
AuthResponse response = new AuthResponse();
response.setSessionID(sessionData.getString("sessionID"));
response.setCustomerID(sessionData.getInteger("customerID"));
response.setOrganizationId(sessionData.getInteger("organizationId"));
// Set customer details
Customer customer = (Customer) sessionData.get("customer");
response.setCustomerName(customer.getFirstName() + " " + customer.getLastName());
response.setCustomerEmail(customer.getEmail());
return response;
} catch (Exception e) {
LOG.error("Login failed for user: " + authRequest.getEmail(), e);
throw new NGOPBaseException("Authentication failed");
}
}
}
Session Management Reality
public PojoMap createNGOPSession(Holder<ResponseStatusHeader> responseStatusHeader,
AuthRequest authRequest, String locale, String source) {
// Get organization ID from code
Integer orgId = getOrganizationId(authRequest.getOrgCode());
// Lookup customer by email + organization
Customer customer = nGOPCusDaoRef.getCustomerByEmail(
authRequest.getEmail(), orgId);
if (customer == null) {
throw new NGOPBaseException("Customer not found");
}
// Verify password (yes, it's custom password hashing)
if (!verifyPassword(customer.getPassword(), authRequest.getPassword())) {
throw new NGOPBaseException("Invalid credentials");
}
// Check if customer is active
if (!customer.getActive()) {
throw new NGOPBaseException("Account is inactive");
}
// Create session ID
String sessionId = generateSessionId();
// Build session data
PojoMap sessionData = new PojoMap();
sessionData.put("sessionID", sessionId);
sessionData.put("customerID", customer.getCustomerId());
sessionData.put("organizationId", orgId);
sessionData.put("customer", customer);
sessionData.put("loginTime", new Date());
sessionData.put("source", source);
// Store in cache (Infinispan)
cacheDataConsumer.put(sessionId, sessionData, SESSION_TIMEOUT_MINUTES * 60);
// Log successful login
auditService.logLogin(customer.getCustomerId(), sessionId, source);
return sessionData;
}
Session Validation Pattern
Every service call validates the session. This is the pattern you'll see everywhere:
// This appears in every service method
public SomeResponse someServiceMethod(Holder<ResponseStatusHeader> responseStatusHeader,
SomeRequest request, String sessionId) {
// Always validate session first
PojoMap sessionData = validateSession(sessionId);
if (sessionData == null) {
throw new NGOPBaseException("Invalid or expired session");
}
// Extract customer context
Integer customerId = sessionData.getInteger("customerID");
Integer organizationId = sessionData.getInteger("organizationId");
Customer customer = (Customer) sessionData.get("customer");
// Proceed with business logic...
}
Customer Management
Customer management handles onboarding, KYC verification, profile updates, and customer lifecycle.
Customer Onboarding Flow
@Override
public CustomerRegistrationResponse registerCustomer(
Holder<ResponseStatusHeader> responseStatusHeader,
CustomerRegistrationRequest request, String locale, String source) {
try {
// Validate organization
Integer orgId = getOrganizationId(request.getOrgCode());
// Check if email already exists
Customer existingCustomer = nGOPCusDaoRef.getCustomerByEmail(
request.getEmail(), orgId);
if (existingCustomer != null) {
throw new NGOPBaseException("Email already registered");
}
// Create customer entity
Customer customer = new Customer();
customer.setFirstName(request.getFirstName());
customer.setLastName(request.getLastName());
customer.setEmail(request.getEmail());
customer.setPassword(hashPassword(request.getPassword()));
customer.setOrganizationId(orgId);
customer.setActive(false); // Requires email verification
customer.setCreatedDate(new Date());
customer.setKycStatus("PENDING");
// Save customer
nGOPCusDaoRef.saveCustomer(customer);
// Generate verification token
String verificationToken = generateVerificationToken();
// Save verification record
CustomerVerification verification = new CustomerVerification();
verification.setCustomerId(customer.getCustomerId());
verification.setVerificationToken(verificationToken);
verification.setExpiryDate(DateUtils.addHours(new Date(), 24));
nGOPCusDaoRef.saveVerification(verification);
// Send verification email
emailService.sendVerificationEmail(customer.getEmail(),
verificationToken, locale);
// Create Salesforce lead (if configured)
if (shouldCreateSalesforceRecord(orgId)) {
salesforceService.createLead(customer);
}
CustomerRegistrationResponse response = new CustomerRegistrationResponse();
response.setCustomerID(customer.getCustomerId());
response.setStatus("PENDING_VERIFICATION");
return response;
} catch (Exception e) {
LOG.error("Customer registration failed", e);
throw new NGOPBaseException("Registration failed: " + e.getMessage());
}
}
KYC Processing
public void processKYCDocuments(Integer customerId, List<KYCDocument> documents) {
Customer customer = nGOPCusDaoRef.getCustomerById(customerId);
if (customer == null) {
throw new NGOPBaseException("Customer not found");
}
// Validate required documents based on organization
KYCRequirements requirements = getKYCRequirements(customer.getOrganizationId());
boolean hasValidId = false;
boolean hasProofOfAddress = false;
boolean hasProofOfIncome = false;
for (KYCDocument doc : documents) {
switch (doc.getDocumentType()) {
case "PASSPORT":
case "DRIVING_LICENCE":
case "NATIONAL_ID":
hasValidId = validateIdDocument(doc);
break;
case "UTILITY_BILL":
case "BANK_STATEMENT":
hasProofOfAddress = validateAddressDocument(doc);
break;
case "PAYSLIP":
case "BANK_STATEMENT_INCOME":
hasProofOfIncome = validateIncomeDocument(doc);
break;
}
}
// Determine KYC status
String kycStatus = "PENDING";
if (hasValidId && hasProofOfAddress) {
if (requirements.isIncomeVerificationRequired() && hasProofOfIncome) {
kycStatus = "APPROVED";
} else if (!requirements.isIncomeVerificationRequired()) {
kycStatus = "APPROVED";
}
}
// Update customer KYC status
customer.setKycStatus(kycStatus);
if ("APPROVED".equals(kycStatus)) {
customer.setActive(true);
customer.setKycApprovedDate(new Date());
}
nGOPCusDaoRef.updateCustomer(customer);
// Notify external systems
if ("APPROVED".equals(kycStatus)) {
salesforceService.updateCustomerStatus(customer, "KYC_APPROVED");
emailService.sendKYCApprovalEmail(customer.getEmail());
}
}
Payment Processing
Payment processing is the heart of Dione's business logic. It handles validation, routing, and integration with external payment systems.
Payment Initiation
@Override
public PaymentResponse initiatePayment(Holder<ResponseStatusHeader> responseStatusHeader,
PaymentRequest request, String sessionId) {
// Validate session and get customer context
PojoMap sessionData = validateSession(sessionId);
Integer customerId = sessionData.getInteger("customerID");
Integer organizationId = sessionData.getInteger("organizationId");
try {
// Validate payment request
PaymentValidationHandler.validatePayment(request, customerId, organizationId);
// Check customer limits
BigDecimal customerLimit = getCustomerLimit(customerId);
if (request.getAmount().compareTo(customerLimit) > 0) {
throw new NGOPBaseException("Amount exceeds customer limit");
}
// Create payment record
Payment payment = new Payment();
payment.setCustomerId(customerId);
payment.setOrganizationId(organizationId);
payment.setAmount(request.getAmount());
payment.setFromCurrency(request.getFromCurrency());
payment.setToCurrency(request.getToCurrency());
payment.setExchangeRate(request.getExchangeRate());
payment.setStatus("PENDING");
payment.setCreatedDate(new Date());
payment.setReference(generatePaymentReference());
// Save payment
paymentDaoRef.savePayment(payment);
// Get FX rate (if not provided)
if (request.getExchangeRate() == null) {
FxRate fxRate = fxRateService.getCurrentRate(
request.getFromCurrency(),
request.getToCurrency(),
request.getAmount(),
organizationId);
payment.setExchangeRate(fxRate.getRate());
}
// Route to appropriate payment processor
PaymentProcessor processor = getPaymentProcessor(
request.getPaymentMethod(), organizationId);
PaymentProcessingResult result = processor.processPayment(payment);
// Update payment status
payment.setStatus(result.getStatus());
payment.setExternalReference(result.getExternalReference());
payment.setProcessedDate(new Date());
paymentDaoRef.updatePayment(payment);
// Send notifications
if ("COMPLETED".equals(result.getStatus())) {
emailService.sendPaymentConfirmation(payment);
smsService.sendPaymentNotification(payment);
}
// Build response
PaymentResponse response = new PaymentResponse();
response.setPaymentId(payment.getPaymentId());
response.setReference(payment.getReference());
response.setStatus(payment.getStatus());
response.setExchangeRate(payment.getExchangeRate());
return response;
} catch (Exception e) {
LOG.error("Payment initiation failed for customer: " + customerId, e);
throw new NGOPBaseException("Payment processing failed: " + e.getMessage());
}
}
Payment Validation Logic
public static void validatePayment(PaymentRequest request,
Integer customerId, Integer organizationId) {
List<String> errors = new ArrayList<>();
// Validate amount
if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
errors.add("Amount must be greater than zero");
}
// Validate currencies
if (StringUtils.isEmpty(request.getFromCurrency()) ||
StringUtils.isEmpty(request.getToCurrency())) {
errors.add("From and To currencies are required");
}
// Validate currency support
if (!isCurrencySupported(request.getFromCurrency(), organizationId)) {
errors.add("From currency not supported");
}
if (!isCurrencySupported(request.getToCurrency(), organizationId)) {
errors.add("To currency not supported");
}
// Validate beneficiary details
if (request.getBeneficiary() == null) {
errors.add("Beneficiary details are required");
} else {
validateBeneficiary(request.getBeneficiary(), errors);
}
// Validate payment method
if (StringUtils.isEmpty(request.getPaymentMethod())) {
errors.add("Payment method is required");
}
// Organization-specific validation
if ("CD".equals(getOrganizationCode(organizationId))) {
validateCurrenciesDirectRules(request, errors);
} else if ("TORFX".equals(getOrganizationCode(organizationId))) {
validateTorFXRules(request, errors);
}
// Compliance checks
if (request.getAmount().compareTo(new BigDecimal("10000")) > 0) {
// Large amount - requires additional verification
Customer customer = customerDao.getCustomerById(customerId);
if (!"ENHANCED".equals(customer.getKycStatus())) {
errors.add("Enhanced KYC required for amounts over 10,000");
}
}
if (!errors.isEmpty()) {
throw new NGOPBaseException("Validation failed: " + String.join(", ", errors));
}
}
FX Trading Engine
The FX trading engine handles currency exchange rates, trade execution, and market data integration.
Rate Management
@Override
public FXRateResponse getCurrentRate(Holder<ResponseStatusHeader> responseStatusHeader,
FXRateRequest request, String sessionId) {
PojoMap sessionData = validateSession(sessionId);
Integer organizationId = sessionData.getInteger("organizationId");
try {
// Check cache first
String cacheKey = buildRateCacheKey(request.getFromCurrency(),
request.getToCurrency(),
request.getAmount(),
organizationId);
FxRate cachedRate = (FxRate) cacheDataConsumer.get(cacheKey);
if (cachedRate != null && !isRateExpired(cachedRate)) {
return buildRateResponse(cachedRate);
}
// Get rate from pricing engine
FxRate fxRate = pricingEngineService.getExchangeRate(
request.getFromCurrency(),
request.getToCurrency(),
request.getAmount(),
organizationId
);
// Apply organization-specific margins
fxRate = applyMargins(fxRate, organizationId);
// Cache the rate
cacheDataConsumer.put(cacheKey, fxRate, RATE_CACHE_TIMEOUT_SECONDS);
// Log rate request
auditService.logRateRequest(sessionData.getInteger("customerID"),
request, fxRate);
return buildRateResponse(fxRate);
} catch (Exception e) {
LOG.error("Rate retrieval failed", e);
// Fallback to last known rate
FxRate fallbackRate = getFallbackRate(request.getFromCurrency(),
request.getToCurrency(),
organizationId);
if (fallbackRate != null) {
LOG.warn("Using fallback rate due to pricing service failure");
return buildRateResponse(fallbackRate);
}
throw new NGOPBaseException("Unable to retrieve exchange rate");
}
}
Trade Execution
public TradeExecutionResult executeTrade(Payment payment) {
try {
// Validate trade parameters
validateTradeParameters(payment);
// Check market hours
if (!isMarketOpen(payment.getFromCurrency(), payment.getToCurrency())) {
throw new NGOPBaseException("Market is closed for this currency pair");
}
// Execute trade with external provider
TradeRequest tradeRequest = new TradeRequest();
tradeRequest.setFromCurrency(payment.getFromCurrency());
tradeRequest.setToCurrency(payment.getToCurrency());
tradeRequest.setAmount(payment.getAmount());
tradeRequest.setRate(payment.getExchangeRate());
tradeRequest.setTradeType("SPOT");
TradeResponse tradeResponse = tradeExecutionService.executeTrade(tradeRequest);
// Update payment with trade details
payment.setTradeReference(tradeResponse.getTradeReference());
payment.setExecutedRate(tradeResponse.getExecutedRate());
payment.setExecutedAmount(tradeResponse.getExecutedAmount());
payment.setTradeDate(new Date());
// Calculate settlement details
SettlementDetails settlement = calculateSettlement(payment);
payment.setSettlementDate(settlement.getSettlementDate());
payment.setSettlementAmount(settlement.getSettlementAmount());
TradeExecutionResult result = new TradeExecutionResult();
result.setStatus("EXECUTED");
result.setTradeReference(tradeResponse.getTradeReference());
result.setExecutedRate(tradeResponse.getExecutedRate());
return result;
} catch (Exception e) {
LOG.error("Trade execution failed for payment: " + payment.getPaymentId(), e);
TradeExecutionResult result = new TradeExecutionResult();
result.setStatus("FAILED");
result.setErrorMessage(e.getMessage());
return result;
}
}
Open Banking Integration
Open Banking services connect to customer bank accounts for transaction retrieval and payment initiation.
Bank Account Connection
@Override
public BankConnectionResponse connectBankAccount(
Holder<ResponseStatusHeader> responseStatusHeader,
BankConnectionRequest request, String sessionId) {
PojoMap sessionData = validateSession(sessionId);
Integer customerId = sessionData.getInteger("customerID");
try {
// Validate bank credentials
if (!isValidBank(request.getBankCode())) {
throw new NGOPBaseException("Unsupported bank");
}
// Connect to Open Banking provider (e.g., Plaid, TrueLayer)
OpenBankingProvider provider = getOpenBankingProvider(request.getBankCode());
// Exchange authorization code for access token
TokenExchangeRequest tokenRequest = new TokenExchangeRequest();
tokenRequest.setAuthorizationCode(request.getAuthorizationCode());
tokenRequest.setClientId(getClientId(provider));
tokenRequest.setClientSecret(getClientSecret(provider));
TokenExchangeResponse tokenResponse = provider.exchangeToken(tokenRequest);
// Get account information
AccountInfoRequest accountRequest = new AccountInfoRequest();
accountRequest.setAccessToken(tokenResponse.getAccessToken());
List<BankAccount> accounts = provider.getAccountInfo(accountRequest);
// Save customer bank accounts
for (BankAccount account : accounts) {
CustomerBankAccount customerAccount = new CustomerBankAccount();
customerAccount.setCustomerId(customerId);
customerAccount.setBankCode(request.getBankCode());
customerAccount.setAccountNumber(account.getAccountNumber());
customerAccount.setSortCode(account.getSortCode());
customerAccount.setAccountType(account.getAccountType());
customerAccount.setAccessToken(encrypt(tokenResponse.getAccessToken()));
customerAccount.setRefreshToken(encrypt(tokenResponse.getRefreshToken()));
customerAccount.setTokenExpiry(tokenResponse.getExpiresAt());
customerAccount.setActive(true);
customerAccount.setCreatedDate(new Date());
bankAccountDaoRef.saveBankAccount(customerAccount);
}
BankConnectionResponse response = new BankConnectionResponse();
response.setConnectionId(generateConnectionId());
response.setAccountCount(accounts.size());
response.setStatus("CONNECTED");
return response;
} catch (Exception e) {
LOG.error("Bank connection failed for customer: " + customerId, e);
throw new NGOPBaseException("Bank connection failed: " + e.getMessage());
}
}
Common Service Patterns
Understanding these patterns will help you navigate and contribute to the Dione codebase effectively.
The Standard Service Pattern
@WebService(serviceName = "WSServiceName",
targetNamespace = "http://services.business.ngop.gtg.currenciesdirect.com/")
public class ServiceNameIMPL implements WSServiceName {
private static final Logger LOG = LoggerFactory.getLogger(ServiceNameIMPL.class);
@Autowired
private BusinessHandlerImpl businessHandlerRef;
@Autowired
private CacheDataConsumer cacheDataConsumer;
@Autowired
private AuditService auditService;
@Override
public ResponseType serviceMethod(Holder<ResponseStatusHeader> responseStatusHeader,
RequestType request, String sessionId) {
// 1. Validate session
PojoMap sessionData = validateSession(sessionId);
Integer customerId = sessionData.getInteger("customerID");
Integer organizationId = sessionData.getInteger("organizationId");
try {
// 2. Validate request
validateRequest(request, customerId, organizationId);
// 3. Delegate to business handler
BusinessResult result = businessHandlerRef.handleBusinessLogic(
request, customerId, organizationId);
// 4. Audit the operation
auditService.logOperation(customerId, "SERVICE_METHOD", request);
// 5. Build response
ResponseType response = new ResponseType();
response.setResult(result.getValue());
response.setStatus("SUCCESS");
return response;
} catch (NGOPBaseException e) {
LOG.error("Business logic error", e);
throw e;
} catch (Exception e) {
LOG.error("Unexpected error in service method", e);
throw new NGOPBaseException("Service unavailable");
}
}
}
Error Handling Pattern
// Standard error handling across all services
try {
// Business logic here
} catch (NGOPBaseException e) {
// Expected business exceptions - log and rethrow
LOG.warn("Business logic exception: " + e.getMessage());
throw e;
} catch (ValidationException e) {
// Validation errors - convert to business exception
LOG.warn("Validation failed: " + e.getMessage());
throw new NGOPBaseException("Validation failed: " + e.getMessage());
} catch (ExternalServiceException e) {
// External service failures - log and provide fallback
LOG.error("External service failure", e);
if (hasFallback()) {
return getFallbackResponse();
} else {
throw new NGOPBaseException("Service temporarily unavailable");
}
} catch (Exception e) {
// Unexpected errors - log and convert to generic exception
LOG.error("Unexpected error in service", e);
throw new NGOPBaseException("Service unavailable");
}
Caching Pattern
// Standard caching pattern used across services
public ExpensiveResult getExpensiveData(String key, Integer organizationId) {
// Build cache key with organization isolation
String cacheKey = "expensive_data:" + organizationId + ":" + key;
// Try cache first
ExpensiveResult cached = (ExpensiveResult) cacheDataConsumer.get(cacheKey);
if (cached != null) {
LOG.debug("Cache hit for key: " + cacheKey);
return cached;
}
// Cache miss - compute the result
LOG.debug("Cache miss for key: " + cacheKey);
ExpensiveResult result = computeExpensiveResult(key, organizationId);
// Cache the result (5 minutes TTL)
cacheDataConsumer.put(cacheKey, result, 300);
return result;
}
Debugging Services
Common debugging scenarios and how to approach them systematically.
Service Call Tracing
# 1. Check JBoss logs for service calls
tail -f /opt/jboss/standalone/log/server.log | grep "serviceMethod"
# 2. Look for session validation errors
grep "Invalid session" /opt/jboss/standalone/log/server.log
# 3. Check for external service timeouts
grep "ConnectTimeoutException\|SocketTimeoutException" /opt/jboss/standalone/log/server.log
# 4. Monitor cache performance
grep "Cache hit\|Cache miss" /opt/jboss/standalone/log/server.log
Common Service Issues
🚫 Session Timeouts
Sessions expire after 30 minutes of inactivity. Check session validation logic.
🚫 External Service Failures
Pricing Engine, Salesforce, or payment processors can be unreliable. Check fallback logic.
🚫 Organization Isolation
Data must be isolated by organization ID. Missing organization checks cause cross-tenant data leaks.
🚫 Cache Inconsistency
Cached data can become stale. Implement proper cache invalidation strategies.
Performance Monitoring
-- Check slow service calls
SELECT
service_name,
AVG(response_time_ms) as avg_response_time,
COUNT(*) as call_count
FROM service_audit_log
WHERE created_date >= DATEADD(hour, -1, GETDATE())
GROUP BY service_name
ORDER BY avg_response_time DESC;
-- Check error rates by service
SELECT
service_name,
COUNT(*) as total_calls,
SUM(CASE WHEN status = 'ERROR' THEN 1 ELSE 0 END) as error_count,
(SUM(CASE WHEN status = 'ERROR' THEN 1 ELSE 0 END) * 100.0 / COUNT(*)) as error_rate
FROM service_audit_log
WHERE created_date >= DATEADD(hour, -1, GETDATE())
GROUP BY service_name
ORDER BY error_rate DESC;
🎯 Key Takeaways
- Session Management: Every service call validates session context
- Organization Isolation: All data access must include organization ID
- Error Handling: Consistent error handling patterns across all services
- External Dependencies: Always implement fallback mechanisms
- Caching: Leverage Infinispan for performance, but handle cache invalidation
- Audit Trails: Log all significant business operations for compliance
📝 Chapter Quiz
Test your understanding of Dione's core services and components: