Breaking the Rules Safely: When a Tech Lead Purposefully Violates the Liskov Substitution Principle (LSP)
Key Takeaways
SOLID is not a religion, and design principles are not immutable commandments. From the perspective of a battle-tested Tech Lead, sometimes deciding to bend the Liskov Substitution Principle (LSP) is a mature choice to keep the system alive. Let’s analyze 4 classic trade-off scenarios and the art of safely isolating the 'toxic code'.

In the theoretical world of Clean Architecture, the Liskov Substitution Principle (LSP) is an absolute truth: "If $S$ is a subtype of $T$, then objects of type $T$ may be replaced with objects of type $S$ without altering any of the desirable properties of the program."
But as a Tech Lead or Senior Engineer, you know all too well that reality is not a sterile laboratory. At 2 AM, when a core system crashes or deadline pressure mounts, stubbornly clinging to textbook "clean code" can kill a project.
Consider an analogy from the oil and gas industry. A small civilian bridge leading to a pumping station displays a sign: "No trucks over 5 tons." In object-oriented terms, Truck inherits from Vehicle. If the bridge cannot accommodate a Truck, that is a clear LSP Violation—the subclass cannot substitute the parent class in that specific context. Yet, that sign exists to save lives and protect infrastructure.
Software engineering is no different. Sometimes, deliberately violating LSP is a mature engineering decision, provided you know how to contain the blast radius.
4 Real-World Scenarios: Trade-offs That Force You to "Break" LSP
1. The Untouchable Legacy System (Oil & Gas / Logistics)
The Situation: You inherit a decade-old SensorDataProcessor interface used to read pressure data from offshore drilling rigs. Now, you need to integrate a new type of IoT sensor (IoTAdvancedSensor). This new sensor returns encrypted data instead of a raw stream, forcing the old getRawStream() method to throw an UnsupportedOperationException.
Why violate LSP?: The legacy system has hundreds of dependencies tightly coupled to that interface. Refactoring the parent interface would trigger a system-wide compile-error chain reaction (Ripple Effect). You do not have three months to re-test an entire drilling rig's software suite.
The Containment Strategy (Adapter Pattern): Do not let clients call the violating class directly. Isolate it.
Java
// Legacy Interface
public interface SensorDataProcessor {
InputStream getRawStream();
void process();
}
// LSP Violation: Forced to throw an exception because the new sensor does not support raw streams
public final class IoTAdvancedSensor implements SensorDataProcessor {
@Override
public InputStream getRawStream() {
// Severe LSP Violation!
throw new UnsupportedOperationException("IoT Sensor uses encrypted MQTT, no raw stream available.");
}
@Override
public void process() {
// Internal decryption and processing logic
}
}
// Containment: Using an Adapter to protect legacy Clients
public class SensorProcessingAdapter {
private final SensorDataProcessor processor;
public SensorProcessingAdapter(SensorDataProcessor processor) {
this.processor = processor;
}
public void safeProcess() {
try {
// Only invoke the stream if it is not a specialized IoT stream
if (!(processor instanceof IoTAdvancedSensor)) {
InputStream is = processor.getRawStream();
// read stream...
}
processor.process();
} catch (UnsupportedOperationException e) {
// Completely isolate the error to prevent a main system crash loop
Log.warn("Bypassed raw stream for IoT Sensor: " + e.getMessage());
}
}
}
2. The Performance-Critical Path
The Situation: You are writing a module to calculate fluid dynamics within a wellbore casing (Wellbore Fluid Dynamics). The system demands real-time processing of 100,000 matrix calculations per second.
Why violate LSP?: According to clean code principles, you should use polymorphism or composition via interfaces like MatrixCalculator. However, excessive virtual table lookups (Dynamic Dispatch) and object allocations trigger massive CPU spikes due to Garbage Collection (GC).
The Containment Strategy (Private Violation, Public Clean): Keep the external interface clean, but use instanceof or raw downcasting inside private methods to optimize CPU cache utilization and assist the JIT Compiler.
Java
public interface CalculationNode {
void execute();
}
public class HighSpeedPipeline {
private List<CalculationNode> nodes;
public void runCriticalPath() {
for (CalculationNode node : nodes) {
// Intentionally breaking abstraction boundaries for performance optimization (JIT Inlining)
if (node instanceof OptimizedGPUComputeNode) {
// Direct downcasting to interface directly with native memory/DirectBuffer
((OptimizedGPUComputeNode) node).executeOnRawMemory();
} else {
node.execute(); // Fallback
}
}
}
}
3. Bittersweet Realities of Third-Party Libraries
The Situation: You are working with a rigid third-party SDK provided by a geophysical measurement hardware vendor. The SDK forces you to extend their BaseClass, but certain parent methods completely break your current business logic.
Why violate LSP?: You do not own their source code. You cannot submit a Pull Request telling them to "fix their LSP."
The Containment Strategy (Wrapper / Facade): Encapsulate the entire library inside a Wrapper. Under no circumstances should that LSP-violating class leak into your core Domain Layer.
Java
// Third-party library class
public class VendorTelemetrySender {
public void sendCoordinate(double x, double y) { ... }
}
// Mandatory violation forced by the vendor SDK
public class CustomTelemetrySender extends VendorTelemetrySender {
@Override
public void sendCoordinate(double x, double y) {
if (x < 0 || y < 0) {
// Forced violation because their SDK cannot handle negative coordinates for offshore rigs
throw new IllegalArgumentException("Offshore coordinates cannot be negative!");
}
super.sendCoordinate(x, y);
}
}
// Containment: Wrapper completely hides this structural flaw
public class TelemetryService {
private final CustomTelemetrySender vendorSender;
public void transmit(GeoLocation location) {
// Sanitize data before passing it to the violating class to prevent runtime exceptions
double safeX = Math.max(0, location.getX());
double safeY = Math.max(0, location.getY());
vendorSender.sendCoordinate(safeX, safeY);
}
}
4. The Temporary Hack for a Looming Deadline
The Situation: Tomorrow is the Ministry's environmental safety audit. The emissions reporting system just exposed a critical logic bug when calculating metrics for a new generation of Jack-up Rigs.
Why violate LSP?: A proper architectural redesign requires 3 days to refactor the class hierarchy. You have 2 hours.
The Containment Strategy (@Deprecated + Technical Debt Ticket): Accept writing the ugliest if-else LSP violation imaginable, but mark it as "radioactive" so it gets cleaned up later.
Java
public class EmissionReport {
public void generate(Rig rig) {
// TEMPORARY HACK: Violating LSP because JackUpRig does not behave like a standard Rig here
if (rig instanceof JackUpRig) {
// TODO: Ticket #OIL-9921 - Refactor Rig hierarchy to support dynamic emission factor
System.out.println("Emergency bypass for Jack-Up Rig emission factor");
injectHackEmission(rig);
return;
}
rig.calculateStandardEmission();
}
@Deprecated(since = "2026-05", forRemoval = true)
private void injectHackEmission(Rig rig) {
// Temporary patch logic for the audit season
}
}
5. Highly Complex & Contradictory Domains (Bounded Contexts)
The Situation: In oil and gas exploration and production (E&P), a Well in the Exploration phase is fundamentally different from a Well in the Production phase. Exploration wells yield no oil; they only produce geological data. If forced into a single parent Well class, calling getYield() on an ExplorationWell explicitly violates LSP.
Why violate LSP?: This stems from an initial design flaw: assuming that because two real-world entities share the same name, they must share the same class in code.
The Containment Strategy (Bounded Context - DDD): Instead of forcing inheritance or restructuring the entire class tree, isolate them into distinct modules or packages (Exploration Context vs. Production Context). Abandon a shared base class when business rules diverge completely.
Final Thoughts from a Tech Lead
Software architecture is not a religion, and SOLID principles are not immutable commandments. They are tools designed to help us manage complexity.
When you decide to place a "no trucks allowed" sign in your code by violating LSP, follow these three rules:
Maintain Awareness: Acknowledge that you are borrowing technical debt.
Build a Containment Wall: Never let an LSP violation leak globally. Use Adapters, Wrappers, or Private Scopes to lock it down.
Document Thoroughly: Leave a tracking ticket, a
@Deprecatedtag, or a detailed comment explaining Why you did it, not just What you did.
A good Senior Developer knows how to apply SOLID. An excellent Tech Lead knows when to break them to ship a project safely.
Have you ever had to break design principles to save a project at the eleventh hour? Share your fire-fighting stories in the comments below.
Related Database Entries

The Black Charter of Oil & Gas: When Code Architecture Dictates Survival
"What if a software design flaw didn't just cause a bug, but cost human lives? Step onto a semi-submersible oil rig to discover why the Open/Closed Principle (OCP) and Dagger 2 aren't just textbook theories—they are a survival guide for mission-critical architecture." Length: 254 characters Best for: Medium, Dev.to, or LinkedIn articles where you want to hook the reader immediately with a story.


DISCUSSION
0 COMMENTS
Be the first to start the discussion.