How JP Morgan, Visa, Goldman Sachs & Morgan Stanley Use OOP: Explained Simply 💳
--
From textbook theory to production systems: real patterns used at the world’s top bank
The same four pillars powering billions of transactions daily using real-world production patterns.
Why most OOP explanations fail you in interviews
Every Java tutorial teaches OOP with a Car class or an Animal class.
class Animal {
void makeSound() { }
}
class Dog extends Animal {
void makeSound() { System.out.println("Woof"); }
}You memorize it. You go to a JP Morgan or Visa interview. They ask:
“Explain OOP with a real backend example from your experience.”
And your mind goes blank. Because nobody told you how OOP actually looks in production financial systems.
This article fixes that. We build one payment system and show all 4 pillars living inside it.
The System: Payment Processing
Every time you tap your card at a store, swipe on Amazon, or send money on PayPal a payment processing system runs. JP Morgan processes 6 billion transactions daily. Visa handles 65,000 transactions per second.
All of it runs on Java. All of it uses these 4 pillars.
Pillar 1 — Encapsulation : Protecting Your Card Data
What it means: Data hiding with controlled access. Private fields. You decide what gets read, what gets written, and with what rules.
Why JP Morgan cares: A bank cannot let any piece of code read raw card numbers or set negative transaction amounts. Encapsulation is not a design choice, it is a security requirement.
public class Payment {
private String cardNumber; // PRIVATE -> nobody reads raw card
private double amount;
private String status;
// controlled read -> last 4 digits only
public String getMaskedCardNumber() {
return "**** **** **** " + cardNumber.substring(12);
}
// controlled write -> validation enforced
public void setAmount(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
this.amount = amount;
}
// status changed ONLY through controlled methods
public void markSuccess() { this.status = "SUCCESS"; }
public void markFailed() { this.status = "FAILED"; }
}What this protects in production:
→ PCI DSS compliance : Raw card number never exposed
→ Not just UI : Negative amounts rejected at object level
→ Only through valid transitions : Status never set arbitrarily
→ Callers never know : Internal implementation can change
In your interview: “Encapsulation in our payment system means card numbers are private . we only expose masked versions. Amount setters validate before setting so negative values never enter the object. This is how real fintech companies achieve PCI compliance at the code level.”
Pillar 2 — Inheritance : Write Once, Used Everywhere
What it means: IS-A relationship. Child class inherits fields and methods from parent. Write common code once , all children get it automatically.
Why Goldman Sachs cares: Goldman has hundreds of transaction types like card payments, wire transfers, bond trades, derivatives settlements. Every single one needs an ID, a timestamp, an audit trail, a soft-delete flag. Writing this in every class is how you get 10,000 lines of duplicated code and 50 bugs when the audit format changes.
// Write ONCE -> inherited by everything
@MappedSuperclass
public abstract class BaseTransactionEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
private String createdBy;
private boolean isDeleted = false;
// common validation -> available to ALL children
public boolean isValid() {
return !isDeleted && createdAt != null;
}
}
// Each transaction type focuses ONLY on what makes it unique
@Entity
public class CardTransaction extends BaseTransactionEntity {
private String cardNumber;
private String merchantId;
// automatically has: id, createdAt, updatedAt, isDeleted, isValid()
}
@Entity
public class WireTransfer extends BaseTransactionEntity {
private String swiftCode;
private String beneficiaryAccount;
// automatically has: id, createdAt, updatedAt, isDeleted, isValid()
}
@Entity
public class WalletTransaction extends BaseTransactionEntity {
private String walletId;
private double walletBalance;
// automatically has: id, createdAt, updatedAt, isDeleted, isValid()
}What this means in production:
- Audit format change? → update BaseTransactionEntity → all 50 types updated
- New transaction type? → extend BaseTransactionEntity → gets everything free
- isValid() logic change? → one place → everywhere
📝 Senior Note — Inheritance vs Composition
Think of Composition as “plug-and-play” logic. While Inheritance locks you into a family tree, Composition lets you build an object by giving it different tools or components.
In a high-stakes environment like JP Morgan or Visa, your business logic changes much faster than your data structure. Composition allows you to swap logic without changing the entire class hierarchy.
The Fraud Check Problem — why Composition wins:
❌ Inheritance Way:
→ BaseProcessor.checkFraud() → ALL children use same fraud logic
→ UpiPayment needs AI fraud? → override parent → parent becomes if-else mess → breaks other children
✅ Composition Way:
→ FraudService interface → inject into processor
→ Need AI fraud for UPI? → swap AiFraudService
→ CardPayment unchanged → WireTransfer unchanged Zero impact
public interface FraudService {
void validate(Payment p);
}
public class AiFraudService implements FraudService {
@Override
public void validate(Payment p) { /* ML model */ }
}
public class BasicRuleFraudService implements FraudService {
@Override
public void validate(Payment p) { /* rule engine */ }
}
public class PaymentProcessor {
private final FraudService fraudService; // COMPOSITION — HAS-A tool
public PaymentProcessor(FraudService fraudService) {
this.fraudService = fraudService;
}
public void execute(Payment p) {
fraudService.validate(p); // just use the tool you were given
// rest of logic
}
}The one liner:
Inheritance = the Skeleton (static structure, fields, identity)
Composition = the Brain (moving parts, logic, behaviour)
In your interview:
“We use Inheritance for BaseTransactionEntity because every transaction IS-A database record with ID and timestamp that never changes. We use Composition for FraudService because fraud logic is behaviour we inject it so we can swap Basic fraud for AI fraud without touching PaymentProcessor. That’s the Goldman Sachs way of keeping structure rigid and logic flexible.”
Pillar 3 — Polymorphism : One Call, Many Behaviours
What it means: Same interface, different behaviour. Caller doesn’t know or care which implementation runs. Same line of code behaves differently based on the actual object.
Why Mastercard and Visa care: A transaction service cannot have a different code path for every payment method. Visa supports 200+ countries, 40+ currencies, and dozens of payment types. If-else chains for each would be unmaintainable. Polymorphism is how payment networks handle this cleanly.
// The contract — same for all payment types
public interface PaymentMode {
boolean processPayment(double amount);
boolean checkBalance(double amount);
String getPaymentType();
}
// Card — its own behaviour
@Component("CARD")
public class CardPayment implements PaymentMode {
@Override
public boolean processPayment(double amount) {
return visaApi.chargeCard(cardNumber, amount); // Visa/Mastercard network
}
@Override
public boolean checkBalance(double amount) {
return creditLimit >= amount;
}
}
// UPI — completely different behaviour, same interface
@Component("UPI")
public class UpiPayment implements PaymentMode {
@Override
public boolean processPayment(double amount) {
return npciApi.transfer(upiId, amount); // NPCI network
}
@Override
public boolean checkBalance(double amount) {
return bankBalance >= amount;
}
}
// PayPal — different again
@Component("PAYPAL")
public class PaypalPayment implements PaymentMode {
@Override
public boolean processPayment(double amount) {
return paypalApi.charge(email, amount); // PayPal network
}
}
// The transaction service — never changes for new payment types
@Service
public class TransactionService {
@Autowired
private Map<String, PaymentMode> paymentModes;
// Spring injects: {CARD: CardPayment, UPI: UpiPayment, PAYPAL: PaypalPayment}
public String process(String type, double amount) {
PaymentMode mode = paymentModes.get(type); // no if-else ✅
if (!mode.checkBalance(amount)) {
return "INSUFFICIENT_BALANCE";
}
boolean success = mode.processPayment(amount); // ONE line
// CARD → Visa/Mastercard network
// UPI → NPCI bank transfer
// PAYPAL → PayPal API
// same line, completely different execution
return success ? "SUCCESS" : "FAILED";
}
}What this means in production:
New payment type added (say, CryptoPay)?
→ Create CryptoPayment implements PaymentMode
→ Add @Component(“CRYPTO”)
→ TransactionService.process() works immediately
→ Zero changes to existing code
This is Open/Closed Principle — open for extension, closed for modification
📝 Senior Flex — Strategy Pattern: This is the Strategy Pattern a behavioural design pattern where the algorithm is selected at runtime. Combined with Spring’s DI map, it eliminates if-else chains and follows the Open/Closed Principle open for extension (new payment type = new class), closed for modification (existing code untouched). Senior interviewers specifically look for this terminology.
In your interview:
“Our TransactionService uses the Strategy Pattern — PaymentMode interface with Card, UPI, PayPal implementations injected via Spring DI map. Same call, different network — Visa, NPCI, or PayPal API. New payment type means one new class, zero changes elsewhere. Open/Closed Principle in production.”
Pillar 4 — Abstraction : Hiding What You Don’t Need to Know
What it means: Showing only what’s necessary. Hiding complexity behind a simple interface. Caller knows WHAT to call not HOW it works.
Why Morgan Stanley cares: Morgan Stanley runs some of the most complex payment and trading flows in the world. A single transaction touches validation, fraud detection, FX conversion, processing, settlement, and regulatory notification in that exact order, every single time. One skipped fraud check at Morgan Stanley’s scale is a regulatory fine. Abstraction enforces governance at the architecture level policy written in code.
public abstract class PaymentProcessor {
// COMPOSITION — processor HAS these tools
@Autowired
private FraudServiceClient fraudClient;
@Autowired
private NotificationService notifyService;
// INHERITANCE — Template Method enforces sequence (Governance)
// final — sequence LOCKED, nobody can reorder or skip
public final PaymentResult execute(Payment p) {
validateAmount(p); // Step 1 — mandatory, private
fraudClient.check(p); // Step 2 — Composition (tool)
double converted = convertFx(p); // Step 3 — mandatory, private
boolean result = process(converted); // Step 4 — Inheritance (child impl)
settle(result); // Step 5 — mandatory, private
notifyService.send(p, result); // Step 6 — Composition (tool)
return new PaymentResult(result);
}
private void validateAmount(Payment p) {
if (p.getAmount() <= 0)
throw new IllegalArgumentException("Invalid amount");
}
private double convertFx(Payment p) {
return fxService.convert(p.getAmount(), p.getCurrency());
}
private void settle(boolean result) { /* settlement engine */ }
// The ONE step each processor customizes
protected abstract boolean process(double convertedAmount);
}
// Card processor — entire Visa complexity hidden here
public class CardProcessor extends PaymentProcessor {
@Override
protected boolean process(double amount) {
// ISO 8583 message, 3DSecure, Visa network routing
// ALL hidden from caller ✅
return visaNetwork.authorize(buildIsoMessage(amount));
}
}
// Wire transfer — entire SWIFT complexity hidden here
public class WireProcessor extends PaymentProcessor {
@Override
protected boolean process(double amount) {
// SWIFT MT103, correspondent banking, AML
// ALL hidden from caller ✅
return swiftNetwork.send(buildMT103(amount));
}
}
// Caller — sees NONE of this complexity
PaymentProcessor processor = new CardProcessor();
PaymentResult result = processor.execute(payment);
// All 6 steps run — fraud always checked — one line ✅📝 Senior Flex : Template Method Pattern + Governance: This is the Template Method Pattern abstract class defines the algorithm skeleton, subclasses fill specific steps. The final keyword is governance: no junior developer adding a new payment processor can skip the fraud check or compliance steps. The Fraud Team can upgrade their entire ML model and Payment Service needs zero redeployment.
Key insight: The sequence is locked via Inheritance. The steps inside are flexible via Composition. Best of both worlds.
In your interview:
“PaymentProcessor uses Template Method Pattern : execute() is final so the 6-step sequence is locked. Fraud check and validation are mandatory , any new processor a junior adds automatically runs all compliance steps. But the fraud logic itself is injected via Composition , the Fraud Team can upgrade their AI model without us redeploying. Governance via Inheritance, agility via Composition.”
The Complete Picture
🚀 Level Up: How Senior Architects Combine the Pillars
In a real system at a bank like JP Morgan or Morgan Stanley, they don’t just pick one pillar , they combine them.
The Governance (the 6-step mandatory flow) is built with Inheritance (Template Method). The Intelligence (the Fraud API, the Notification Service) is plugged in via Composition.
- Inheritance locks the rules → juniors can’t break compliance
- Composition keeps services decoupled → Fraud team updates their ML model, Payment Service needs zero redeploy
The Senior Answer when asked about governance:
“The governance locking the 6-step sequence is achieved through Inheritance using the Template Method Pattern. By making execute() final, the skeleton is unchangeable. However, individual steps like Fraud Check are handled via Composition. We inject a FraudServiceClient , the Fraud Team can update their entire API, change their ML models, add new rules and our Payment Service doesn’t need a single code change or redeploy. Inheritance gives us a rigid safe structure. Composition gives us flexible swappable logic inside that structure.”
What Makes Senior Engineers Different
1. Encapsulation
- Junior answer: “Encapsulation is about using private fields and creating getters and setters to access them.”
- Senior answer: “Encapsulation is our first line of defense for PCI DSS compliance. By keeping raw card data private and only exposing masked strings, we ensure that sensitive information never leaks into the logging layer or unauthorized services.”
2. Polymorphism
- Junior answer: “Polymorphism means same method, different behaviour.”
- Senior answer: “We leverage Polymorphism via the Strategy Pattern. By injecting a map of payment modes through Spring, we strictly follow the Open/Closed Principle. This allows us to integrate a new payment provider by simply adding a new class, rather than modifying a massive
if-elseblock in our core service."
3. Abstraction
- Junior answer: “Abstraction is just hiding the implementation details from the user.”
- Senior answer: “Abstraction is Architectural Governance. By using a Template Method with a
finalexecution flow, we bake company policy into the code. This ensures that critical steps, like fraud detection and currency conversion, are physically impossible for a junior developer to skip when they implement a new processor."
4. Inheritance
Junior answer: “Inheritance allows us to reuse code by extending a parent class."
Senior answer: “Inheritance is for the Skeleton: shared identity, database IDs, and audit fields. For the Brain, we use Composition. Understanding that inheritance is for ‘who you are’ and composition is for ‘how you behave’ is the primary distinction between a mid-level developer and an architect.”
The “So What?” Factor
The takeaway is simple: don’t just explain the tool; explain the risk it mitigates.
When you stop talking about “Animals and Dogs” and start talking about Compliance, Governance, and the Open/Closed Principle, you stop being a coder and start being an asset to the business. That is the shift that gets you the offer.
The 25-Second Interview Answer
“In our payment architecture, Encapsulation serves as our primary security layer: we protect sensitive card data with private fields and expose only masked getters to maintain PCI compliance. Inheritance provides our structural skeleton; all payment entities extend a base entity so that audit fields are written once but inherited everywhere.
To keep the system flexible, we use Polymorphism via the Strategy Pattern. By utilizing a Spring DI map, the TransactionService can trigger the correct network—be it Visa, NPCI, or SWIFT—through a single interface call, effectively eliminating if-else debt and honoring the Open/Closed Principle. Finally, we treat Abstraction as architectural governance. We implement the Template Method Pattern with a final execution flow to lock in mandatory fraud checks, while keeping the specific logic decoupled so our teams can deploy updates independently.””
Found this useful? Follow for more real-world Java concepts used in fintech.