Skip to content

Why Security Engineers Prefer Simple Code Over Deep Abstraction

The Problem

I was reviewing a security-critical authentication system when I hit a wall. The code used deep inheritance hierarchies, abstract factories, and dependency injection. Tracing a single request required jumping through 12 files. I couldn’t answer the simple question: “Is this request validated before it reaches the database?”

That’s when I understood why security engineers prefer simple code. Deep abstraction doesn’t just make code harder to read. It creates hiding spots for vulnerabilities.

My Initial Misunderstanding

I used to think security engineers were behind the times. They seemed to reject modern patterns, prefer flat functions over elegant hierarchies, and avoid dependency injection frameworks. I assumed they just hadn’t learned better patterns.

I was wrong. They had learned those patterns and found them dangerous.

The Core Insight: Abstraction Hides Bugs

One security engineer (score: 38 on Reddit) put it bluntly:

“There’s a reason security stuff is mostly declarative, a very good one…”

Another developer (score: 1) added the key insight:

“A lot of security work values simple, explicit, easy-to-audit code over deep class hierarchies. Security engineers understand that deep abstraction and complexity hides more bugs than anything else.”

Let me show you what this means in practice.

What Goes Wrong: A Real Example

I wrote what I thought was clean, pattern-following code for request handling:

BaseHandler.java
abstract class BaseHandler {
protected abstract void process(Request req);
protected void validate(Request req) {
if (req.data == null) throw new ValidationException();
}
public void handle(Request req) {
validate(req); // Is this always called?
process(req);
}
}

This looked reasonable. The base class guarantees validation. But then a teammate added:

BypassHandler.java
class BypassHandler extends BaseHandler {
@Override
public void handle(Request req) {
// SECURITY BUG: Skips validation by not calling super
process(req); // Direct call, validate() bypassed!
}
}

The bug was invisible in code review. We only found it during a security audit when someone noticed unvalidated data reaching the database. The abstraction gave us false confidence.

Why This Matters for Security Audits

Security code must answer a simple question: “Can untrusted input reach a sensitive operation without validation?”

In complex OOP code, answering this requires:

Audit Complexity with Deep Abstraction
1. Find all subclasses of BaseHandler
2. Check each one's handle() method
3. Verify which call super.handle()
4. Check if validate() is overridden
5. Trace virtual dispatch possibilities
6. Consider future subclasses (unknown unknowns)

Compare to simple, explicit code:

handler.go
func handleRequest(req *Request) error {
// Step 1: Validate (explicit, cannot be bypassed)
if err := validate(req); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Step 2: Sanitize (visible in control flow)
sanitized := sanitize(req.Data)
// Step 3: Process (no hidden behavior)
return process(sanitized)
}

One glance tells you: validation happens before processing. No subclasses to check. No virtual dispatch to trace.

The Attack Surface Problem

Another developer (score: 20) highlighted two key benefits:

“Minimal dependencies (less attack surface), Readability for the next person who has to audit it.”

Let’s compare attack surfaces:

Attack Surface Comparison
Complex OOP:
├── AbstractBaseHandler (1 class)
│ ├── AbstractSecureHandler (1 class)
│ │ ├── ConcreteHandlerA (1 class)
│ │ └── ConcreteHandlerB (1 class)
├── DependencyInjector (1 class)
│ ├── Container (1 class)
│ └── 50+ transitive dependencies
└── ORMMapper (1 class + 10 plugins)
Total: ~70 components to audit
Simple Code:
├── handlers/
│ ├── auth.go (1 file, 150 lines)
│ └── process.go (1 file, 200 lines)
└── vendor/
└── 5 vetted dependencies
Total: 7 files, ~1000 lines to audit

Each component is a potential attack vector. More components = more places for bugs to hide.

My Journey: From Annoyance to Appreciation

A developer (score: 2) described my exact experience:

“The simplicity that annoyed me at first ended up being the thing I liked most about it for security work, less abstraction means fewer places for bugs to hide.”

I went through phases:

  1. Week 1: “This code is primitive. Where are the patterns?”
  2. Week 2: “I guess it’s easier to understand, but it’s repetitive”
  3. Month 1: “Oh, I can audit this entire file in 10 minutes”
  4. Month 3: “I found a critical bug because the code was obvious”

Now I write simple code by default and add abstraction only when necessary.

The Declarative Pattern: Security’s Preferred Approach

Security tools overwhelmingly use declarative approaches. Look at firewall rules:

firewall-rules.yaml
rules:
- allow: port 443 from 0.0.0.0/0
- deny: all

Or access control:

rbac-policy.yaml
roles:
admin:
permissions: [read, write, delete]
viewer:
permissions: [read]

Why? Because declarative code:

  1. Has finite, enumerable states
  2. Has no hidden control flow
  3. Can be audited without execution
  4. Is immutable by default

Common Mistakes: Over-Engineering Security

I’ve made all these mistakes:

Mistake 1: Burying security logic in inheritance

OverEngineeredSecurity.java
// Wrong: Security logic buried 3 layers deep
class SecurityManager extends AbstractSecurityBase
extends BaseComponent implements ISecurity {
// Good luck finding the actual validation logic
}

Mistake 2: Pattern overkill for simple tasks

OverEngineeredValidation.java
// Wrong: 100 lines to validate an email
interface IValidator<T> {
ValidationResult validate(T input);
}
abstract class AbstractValidator<T> implements IValidator<T> {
protected abstract ValidationRule<T> getRule();
// ... 5 more abstract methods
}
class EmailValidator extends AbstractValidator<String> {
// Why is this so complicated?
}

The right approach:

validate-email.go
// Right: Simple, auditable, complete
func validateEmail(email string) error {
if len(email) > 254 {
return errors.New("email too long")
}
if !emailRegex.MatchString(email) {
return errors.New("invalid email format")
}
return nil
}

Static Analysis: Why It Matters

Security teams rely heavily on static analysis tools. Complex OOP code breaks these tools:

Static Analysis Effectiveness
Metric | Complex OOP | Simple Code
--------------------|------------|-------------
Path coverage | 20-40% | 80-95%
False positives | High | Low
Taint tracking | Difficult | Straightforward
Dependency scanning | Complex | Flat list

When I write security code now, I ask: “Can a static analyzer verify this?” If the answer is no, I simplify.

Real-World Examples

OpenBSD’s Approach:

OpenBSD, known for security, follows:

  1. Code simplicity over features
  2. Auditable code over clever abstractions
  3. Fewer dependencies over more features

Go’s Standard Library:

go-crypto-simple.go
// Simple API, hard to misuse
key := sha256.Sum256([]byte("secret"))
block, _ := aes.NewCipher(key[:])
// No inheritance, no factories, no DI
// Just functions that do one thing

Rust’s Security Advantage:

rust-ownership.rs
// Compiler enforces single owner
// No hidden shared state across class hierarchies
fn process_secure(data: Vec<u8>) -> Result<Processed, Error> {
// data is consumed, cannot be used again
// No side effects possible from other code
Ok(Processed::from(data))
}

When Abstraction Is Actually Needed

I’m not saying abstraction is always bad. Abstraction is appropriate when:

  1. The complexity you’re hiding is truly irrelevant to security
  2. The abstraction reduces overall attack surface
  3. The abstraction is simple enough to fully audit

For example, a TLS library that hides certificate validation details is fine. The abstraction surface is well-defined and small. But a “base handler” with 10 subclasses where each might or might not validate? That’s a security problem.

Practical Guidelines

After years of writing and auditing security code, here’s what I follow:

Security Code Guidelines
1. Prefer functions over classes
2. Prefer flat over hierarchical
3. Prefer explicit over implicit
4. Prefer composition over inheritance
5. Prefer small dependencies over frameworks
6. Prefer declarative over imperative
7. Prefer pure functions over stateful objects

Summary

Security engineers prefer simple code because complexity is the enemy of security. Every abstraction layer creates potential hiding spots for bugs, vulnerabilities, and unexpected behavior.

The shift from annoyance with simplicity to appreciation is common. What initially seems limiting becomes essential. Fewer places for bugs to hide means faster reviews, more confident deployments, and ultimately, more secure systems.

For your next security-sensitive code, try this: start simple, add abstraction only when you can prove it reduces risk. Your future auditors (and attackers) will have less room to hide problems.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments