The Rise of the Forward Deployed Engineer

For years, software engineering has largely been optimized around one thing: producing code.

Better frameworks, better abstractions, better tooling, better infrastructure. Every wave of innovation pushed engineers toward higher leverage. We moved from assembly to high-level languages, from monoliths to cloud platforms, from manual provisioning to infrastructure as code.

Now AI is accelerating this trend dramatically.

But contrary to popular narratives, AI is not reducing the need for engineers. It is changing where engineering value lives.

As implementation becomes cheaper and increasingly automated, the bottleneck is shifting elsewhere: understanding systems, navigating ambiguity, integrating with real-world workflows, and orchestrating increasingly complex socio-technical environments.

This is why the Forward Deployed Engineer is returning to the center of the industry.

Not as a niche role.

As a new model for engineering itself.

The End of Coding as the Primary Bottleneck

Historically, shipping software was expensive because implementation was expensive.

Writing APIs, building interfaces, connecting systems, implementing workflows, debugging edge cases. Most of the engineering effort went into transforming ideas into functioning software.

AI changes this equation.

Today, large portions of implementation can already be delegated:

  • boilerplate generation
  • scaffolding
  • refactoring
  • testing
  • documentation
  • integration assistance
  • debugging support

The cost of producing software is collapsing.

But the cost of understanding reality is not.

And that changes everything.

The difficult part is no longer simply writing the system. The difficult part is deciding:

  • what should exist
  • where intelligence should live
  • how humans interact with the system
  • how workflows evolve
  • which trade-offs matter
  • how reliability, adoption, and operational constraints interact

The engineer’s role is moving upward, from implementation toward orchestration.

Not away from technology.

Closer to the system as a whole.

Why Forward Deployed Engineers Make Sense in the AI Era

This is precisely where the Forward Deployed Engineer becomes critical.

The original rise of the FDE role came from companies like Palantir, where engineers operated directly inside complex customer environments. They were not simply implementing tickets from afar. They were embedded inside operational realities, understanding problems in context and adapting systems continuously.

At the time, this approach looked unusual compared to traditional software organizations built around centralized product teams and standardized platforms.

Today, AI makes this model dramatically more relevant.

Because modern AI systems are not static products.

They are evolving operational systems tightly connected to:

  • business processes
  • human workflows
  • internal knowledge
  • infrastructure constraints
  • organizational behavior
  • data quality
  • trust and adoption

The challenge is no longer just building software.

It is integrating intelligence into messy real-world environments.

That requires engineers who can move across abstraction layers:

  • technical implementation
  • product reasoning
  • operational understanding
  • workflow design
  • systems thinking
  • customer context

This is exactly the terrain where Forward Deployed Engineers operate.

The Return of the Generalist Engineer

For years, engineering organizations increasingly optimized around specialization.

Frontend engineers. Backend engineers. Platform engineers. DevOps engineers. Data engineers. ML engineers.

This specialization made sense in a world where software complexity itself was the dominant challenge.

But AI changes leverage.

When implementation accelerates, the highest leverage engineer is often no longer the one who knows the narrowest domain in extreme depth.

It becomes the engineer who can understand the broader system fastest.

Not because deep expertise stops mattering.

But because isolated expertise is no longer sufficient.

The emerging high-leverage engineer is someone capable of:

  • understanding business constraints
  • reasoning about systems holistically
  • delegating implementation to AI tools
  • validating outputs critically
  • orchestrating workflows across teams and technologies
  • iterating directly against real-world feedback

The Forward Deployed Engineer is essentially this model formalized.

An engineer operating at a higher level of system abstraction.

AI Expands the Engineer’s Surface Area

One of the biggest misconceptions around AI is the idea that engineers will simply “code less.”

That framing misses the real transformation happening.

AI does not reduce engineering complexity.

It redistributes it.

A modern engineer can now influence far larger systems than before because implementation scales differently. A single engineer equipped with AI tooling can prototype faster, integrate faster, iterate faster, and explore more solution spaces than previously possible.

But this expanded leverage comes with a new requirement: broader system understanding.

The engineer is increasingly becoming:

  • architect
  • integrator
  • operator
  • workflow designer
  • human-in-the-loop orchestrator

In many organizations, this starts looking surprisingly similar to the Forward Deployed Engineer model.

Not because engineers are becoming less technical.

But because technical value is moving closer to real-world system design.

Why American AI Companies Are Hiring This Role Aggressively

This shift explains why many leading AI companies are aggressively hiring Forward Deployed Engineers or equivalent profiles.

Once foundational models become widely accessible, competitive advantage no longer comes purely from model access.

It comes from integration.

The real moat becomes:

  • embedding AI into operational workflows
  • connecting fragmented systems
  • adapting to customer environments
  • creating reliable human-AI feedback loops
  • driving adoption inside organizations
  • iterating quickly against real-world constraints

This work cannot be solved purely from a centralized product roadmap.

It requires engineers capable of operating directly at the intersection of systems, users, infrastructure, and business reality.

The Forward Deployed Engineer is uniquely positioned for this environment.

Engineering Is Expanding Into the Real World

Software engineering is not disappearing.

But the center of gravity is shifting.

For decades, engineering value concentrated around the production of software itself. AI is now commoditizing parts of that production layer.

As a result, value is moving upward:

  • toward systems thinking
  • toward orchestration
  • toward operational understanding
  • toward integration
  • toward real-world complexity

The future engineer may manually write less code than before.

But they may shape larger systems, influence broader workflows, and operate with more leverage than ever before.

The rise of the Forward Deployed Engineer is not a temporary hiring trend.

It is a signal that software engineering itself is evolving from coding systems to understanding systems.

How Would I Build a Payment System That Doesn’t Lose Money

This is the first post in a series I’m calling How Would I Build. I take a real engineering problem, reason through it in plain language first, then attach the actual name for that reasoning at the end. Jargon comes last, not first.

I’ll be upfront, I didn’t sit down and immediately know all the answers here. A lot of this came from asking “okay but why would that break” until something clicked. That’s the energy I’m going for.

Starting with this: how would you build a payment system that handles 10,000 transactions per second without losing a single one?

Two customers, one suya guy

There’s a suya spot near you. One guy grilling. Two customers walk up at the same time and both want the last stick.

The suya guy has two hands. He can’t serve both customers simultaneously. One of them has to wait.

Now imagine he ignores that and just gives both customers the same stick. That’s your payment bug.

Two people hit withdraw on the same wallet at the exact same time. Both requests check the balance. Both see enough money. Both go through. You’ve just paid out twice what was in the account.

My first instinct was just — lock it. But I had to figure out what “lock it” actually meant before it made sense.

The answer is: whoever gets there first locks the wallet in the database before the second request can even read the balance. The second request waits. When the first one finishes, the second one checks the balance again and finds there’s nothing left.

That whole situation — multiple operations running at the same time and stepping on each other, is a concurrency problem. The specific bug where both read stale data before either writes is a race condition.

The suya guy gets popular. He buys a bigger grill.

Business is good now. The queue is long. The suya guy thinks: I need a bigger grill. More charcoal. Better equipment. He upgrades everything.

It helps. Orders come out faster. But the queue is still there. Because no matter how big the grill is, there’s still one person running it. Customers still wait behind each other. And there’s a limit to how big a grill can get anyway.

That’s vertical scaling. You give the same server more power — more CPU, more RAM, faster storage. It buys you time but it doesn’t solve the problem. The ceiling just moves higher.

At some point the suya guy realizes the grill isn’t the problem. One location is the problem.

So he opens a second spot. Then a third. Now three customers are being served at the same time across three locations. The individual grills are smaller but the total output is way higher.

That’s horizontal scaling. More machines, not a bigger machine. The limit is now cost, not hardware.

But not everything needs the main guy

The suya guy notices something. Most people walking up aren’t placing new orders. They’re just checking if their order is ready. Those people are slowing down the main grill.

So he puts a separate person to handle “is my order ready” questions. That person has a copy of the order list and handles all status checks. The main guy now only deals with new orders.

Read traffic stops choking write performance.

That’s read replicas. The main database handles writes. Copies of it handle reads.

But here’s where I got confused for a bit. Replicas don’t fix everything. If 10,000 new orders are coming in per second, the main grill still has a ceiling on how many it can process. Adding more status-check people doesn’t change that.

To scale new orders, you need more main grills. Not copies — actual separate locations taking orders. Customers 1 to 2,000 go to location one. 2,000 to 4,000 go to location two. New orders are now spread across multiple spots.

That’s sharding.

What if someone at spot one wants to pay for a friend at spot three

Someone at spot one wants to cover their friend’s order at spot three. The suya guy at spot one collects the money. Before he can radio spot three to release the order, his phone dies. Spot three never gets the message. The money is gone and the friend got no suya.

My instinct was to get both spots to agree before anything moves. Spot one holds the money, spot three holds the order, and only when both confirm does anything proceed. That works but it’s slow. And if the person coordinating between them disappears mid-process, both spots are frozen waiting for a signal that never comes.

The simpler approach is to not wait for both at the same time. Spot one takes the money and writes down “I took this, spot three owes a release.” If spot three never confirms, spot one automatically reverses the charge and refunds the customer. No coordination needed upfront. Each spot handles its own piece and knows exactly what to undo if something breaks.

That pattern is called a SAGA. Failure recovery for distributed operations. Each step has a compensating action ready before anything runs.

Multiple spots, but who tells customers where to go

You’ve opened five suya spots. A new customer walks up. They see five spots and have no idea which one has the shortest queue. They just stand there confused.

So you put someone at the entrance whose only job is to look at all five spots and point each new customer to the next free one. The customer doesn’t think about it. They just get directed.

That’s a load balancer. It sits in front of your servers and distributes incoming traffic so no single server carries everything. Nginx handles this well.

But now the person at the entrance is a single point of failure. They go down, everyone is lost.

So you have a backup person standing right next to them. If the first one goes down, the backup steps in automatically. That switch is called failover. Running both is a high availability setup.

Rate limiting sits on top of this. You cap how many orders one person can place per minute. It won’t solve legitimate volume but it stops bad actors from making the problem worse.

Something goes wrong and you have no idea which spot caused it

At 10,000 transactions per second, a failed transaction could have broken at the entrance, the order taker, the payment handler, the records person, or the notification sender. Logs scattered across twenty servers.

I genuinely didn’t think about this until I asked myself — if something breaks at this scale, where do I even start looking?

The fix is to give every order a unique ticket number the moment it enters the system, then write that ticket number into every record that order touches across every spot.

When something breaks, you search for that ticket number and see the full journey of that order in one place. Every step, every spot, in order.

That ticket number is called a correlation ID. Following a request across services using it is called distributed tracing.

Tools like Datadog or the ELK stack pull logs from all your servers into one searchable place. Elasticsearch stores the logs. Kibana is the dashboard you use to search them.

What the full picture looks like

A request comes in. The load balancer directs it to one of several servers, with a standby ready if the active one dies. Rate limiting filters abuse. The server processes the transaction with pessimistic locking to prevent race conditions. Reads hit replicas, writes hit the primary. Sharding distributes write load across multiple database nodes. Cross-spot transactions use SAGA so failures don’t leave money in limbo. Every step logs with a correlation ID that flows into a centralized system where any transaction can be traced end to end in seconds.

That’s a payment system that doesn’t lose money.

I’m Damola, a backend engineer. Find the rest of this series on GitHub. Follow me on Dev.to for the next one.

Building Calendar ToDo: Turning Calendar Events into a Done List with SwiftUI and EventKit

Calendar ToDo

Sometimes I want to look back at my calendar and know not only what I planned, but what I actually did.

For example:

  • Did I finish my study session?
  • Did I actually go to the gym?
  • Which scheduled tasks did I complete this week?
  • What kind of rhythm do I have in my daily schedule?

Calendar apps are great for managing future plans.

But I felt they were not very good at showing whether those plans were actually completed.

So I started building an iOS app called Calendar ToDo.

It is an app that lets you mark calendar events as done and turn your calendar into a personal execution log.

App Store: Calendar ToDo

Calendar ToDo

What is Calendar ToDo?

Calendar ToDo is an iOS app that adds completion history to your existing calendar events.

Calendar ToDo - Design

The main idea is simple:

Your calendar already contains your plans.

Calendar ToDo helps you turn those plans into a done list.

The app can:

  • Show today’s calendar events
  • Mark events as done
  • Show today’s completion rate
  • Keep a timeline of completed events
  • Visualize your execution rhythm with a heatmap
  • Send a notification when an event ends
  • Let you mark an event as done from the notification
  • Sync completion history with iCloud / CloudKit

The important design decision is this:

Calendar ToDo does not edit your original calendar events.

It reads calendar data using EventKit, then stores completion records separately inside the app.

Why not just edit the calendar title?

One simple way to manage completed calendar events is to add a checkmark to the event title.

For example:

✅ Study SwiftUI
✅ Workout
✅ Write blog post

This works for a small personal calendar, but it has some problems.

Editing calendar titles Calendar ToDo
Modifies the original calendar Keeps the calendar read-only
Mixes plans and results Stores completion history separately
Can be risky with shared calendars Does not touch shared calendar data
Can break with recurring events Tracks each occurrence separately

For work calendars, shared calendars, and recurring events, editing the original event title can feel unsafe.

I wanted Calendar ToDo to behave more like a layer on top of the calendar.

The calendar remains the source of planned events.

The app stores only the execution result.

Basic architecture

The app is built with SwiftUI, EventKit, Core Data, and CloudKit.

A simplified data flow looks like this:

SwiftUI
  ↓
EventKit
  ↓
Read calendar events
  ↓
Core Data / CloudKit
  ↓
Store completion history
  ↓
Notifications / Widgets / Reports

EventKit is used to read calendar events.

Core Data and CloudKit are used to store and sync the app’s own completion records.

The key separation is:

  • Calendar events: fetched from EventKit
  • Completion records: stored by Calendar ToDo

This separation keeps the user’s calendar safe.

Reading events with EventKit

Fetching today’s events with EventKit looks roughly like this:

let eventStore = EKEventStore()

let calendars = eventStore.calendars(for: .event)

let predicate = eventStore.predicateForEvents(
    withStart: startOfDay,
    end: endOfDay,
    calendars: calendars
)

let events = eventStore.events(matching: predicate)

The fetched EKEvent objects are displayed in SwiftUI.

But the app does not modify them directly.

That rule is important because the calendar may contain work events, shared events, recurring events, or events created by other apps.

Storing completion history separately

Instead of writing completion state back to EKEvent, the app creates its own completion record.

A simplified model looks like this:

struct CompletionRecord {
    let occurrenceKey: String
    let status: CompletionStatus
    let completedAt: Date
}

enum CompletionStatus {
    case done
    case skipped
    case partial
}

The actual implementation has more fields, but the core idea is the same:

Do not change the calendar event.

Store the result separately.

This makes the app more privacy-friendly and safer to use with existing calendars.

The difficult part: matching events and records

At first, it seems easy to connect a completion record with an EventKit event.

You might think:

event.eventIdentifier

But in practice, this is not enough.

Calendar data is more complicated than it looks.

Some examples:

  • Recurring events can share identifiers
  • A single occurrence can be moved
  • Events can be deleted and recreated
  • Start and end times can change
  • iCloud sync can create timing differences
  • The same event may appear on multiple devices

So the app needs a way to identify “this specific event on this specific day.”

A simplified key might look like this:

struct OccurrenceKey: Hashable {
    let calendarItemIdentifier: String
    let startDate: Date
    let endDate: Date
}

This is one of the hardest parts of the app.

A calendar looks simple in the UI, but once you try to build reliable history on top of it, you need to think carefully about recurring events, time zones, updates, and sync behavior.

Completing events from notifications

Another important feature is the ability to record completion without opening the app.

When an event ends, Calendar ToDo can send a notification.

The notification has actions such as:

  • Done
  • Snooze for 1 hour

This matters because recording should be lightweight.

If users need to open the app every time, many records will be missed.

A small notification action can make the difference between:

“I planned this.”

and:

“I actually did this.”

Why build this as a calendar-based app?

Traditional to-do apps are great for managing tasks.

But I think calendars have one strong advantage:

Calendar events already contain time context.

A task says what you need to do.

A calendar event says when you planned to do it.

That time context is useful for reflection.

For example:

  • Which time of day do I complete the most tasks?
  • Are morning plans easier to finish?
  • Do I often leave evening events unfinished?
  • Which recurring events are stable?
  • Which scheduled activities tend to move or disappear?

This is why I think Calendar ToDo is not just a to-do app.

It is closer to an execution log built on top of your calendar.

Design principles

While building the app, I try to follow three principles.

1. Do not break the calendar

The app should not add, edit, or delete calendar events.

Many users already rely on their calendar for work, school, family, or shared schedules.

The app should respect that.

2. Make recording easy

The value of the app depends on whether users can keep recording.

So completion should be fast.

Notification actions, simple buttons, and lightweight states are more important than complex input forms.

3. Return value to the user

If the app stores completion history, that data should be useful to the user.

The goal is not just to collect records.

The goal is to help users see their rhythm, understand their schedule, and reflect with less effort.

Future ideas

I am currently exploring features such as:

  • Partial progress records
    • a little progress
    • halfway done
    • almost done
  • Reflection tags
  • Daily schedule load visualization
  • Schedule change detection
  • Weekly reports
  • Widgets
  • Better ways to summarize execution patterns

Not every scheduled event is simply “done” or “not done.”

Sometimes you made progress.

Sometimes the event was too large.

Sometimes the timing was wrong.

Sometimes the environment was not good.

I want Calendar ToDo to capture those states in a positive and lightweight way.

What I learned

The biggest thing I learned is that calendars are much more complex than they look.

Reading events is easy.

Building a reliable history layer on top of calendar events is much harder.

You need to think about:

  • Event identity
  • Recurring events
  • Time zones
  • Deleted events
  • Changed start times
  • Cloud sync
  • Notification timing
  • Privacy
  • User trust

But I still think this idea is worth exploring.

A calendar should not only be a place for future plans.

It can also become a record of what you actually did.

Conclusion

I built Calendar ToDo because I wanted to mark calendar events as done without modifying the original calendar.

The main idea is:

Keep the calendar read-only.

Store completion history separately.

Turn scheduled events into an execution log.

If you are building an iOS app with EventKit, SwiftUI, Core Data, or CloudKit, I hope this article gives you some useful ideas.

You can try Calendar ToDo here:

https://apps.apple.com/us/app/calendar-todo/id6756511434

Thanks for reading.

RabbitMQ vs Kafka: Choosing the Right Messaging System for Real Backend Architectures (part-3)

This is my final part-3 of the series. I recommend you to read previous articles of the series.

In this article, I’d like to give sample code snippets for RabbitMQ & Kafka with Spring Boot.

9. Spring Boot Integration Examples

Messaging systems make a lot more sense once you see how they actually behave inside applications.

This section is not about building a full production-ready setup.

The goal here is simpler:
show how RabbitMQ and Kafka integrations usually feel different inside Spring Boot apps.

RabbitMQ Integration Example

RabbitMQ integration in Spring Boot is usually pretty simple and workflow-focused.

A typical flow looks something like this:

  • order gets created,
  • app publishes a processing task,
  • consumer picks it up and runs business logic.

Producer Example

@Service
public class OrderPublisher {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void publish(OrderCreatedEvent event) {
        rabbitTemplate.convertAndSend(
                "order.exchange",
                "order.created",
                event
        );
    }
}

Here:

  • the exchange handles routing,
  • routing keys decide where messages go, and
  • RabbitMQ distributes messages to queues.

This routing flexibility is one of RabbitMQ’s biggest strengths.

Consumer Example

@Component
public class OrderConsumer {

    @RabbitListener(queues = "order.processing.queue")
    public void process(OrderCreatedEvent event) {

        System.out.println("Processing order: " + event.orderId());

        // Business logic
    }
}

This style works really well for things like:

background jobs,
workflow execution,
notifications, and
transactional async tasks.

The queue basically acts like a work dispatcher.

Retry & DLQ Configuration

One reason RabbitMQ is popular in backend systems is its retry handling.

A common production setup usually includes:

  • main queue,
  • retry queue,
  • dead-letter queue (DLQ).
@Bean
public Queue orderQueue() {
    return QueueBuilder.durable("order.processing.queue")
            .deadLetterExchange("order.dlx")
            .build();
}

In real systems:

  • temporary failures go through retry flows,
  • poison messages move into DLQs, and
  • teams get visibility into failed processing.

You’ll see this pattern everywhere in enterprise systems.

Kafka Integration Example

Kafka integration feels different because Kafka itself works differently.

Instead of queue-based task distribution, Kafka is built around event streams and partitioned logs.

Producer Example

@Service
public class OrderEventPublisher {

    @Autowired
    private KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate;

    public void publish(OrderCreatedEvent event) {

        kafkaTemplate.send(
                "order-events",
                event.orderId(),
                event
        );
    }
}

Notice this part:

event.orderId()

That’s the partition key.

And it matters a lot.

Kafka guarantees ordering only inside a partition.

Using the order ID as the partition key ensures:

  • all events for the same order,
  • stay inside the same partition, and
  • remain ordered.

Partition strategy becomes a huge design topic in Kafka systems.

Consumer Example

@Component
public class OrderEventConsumer {

    @KafkaListener(
            topics = "order-events",
            groupId = "order-processing-group"
    )
    public void consume(OrderCreatedEvent event) {

        System.out.println("Processing order event: "
                + event.orderId());

        // Business logic
    }
}

Unlike RabbitMQ:

  • Kafka consumers track offsets,
  • messages stay in the log, and
  • multiple consumer groups can process the same events independently.

That means:

  • analytics services,
  • audit systems,
  • notification services,
  • reporting pipelines

can all consume the same event stream separately.

This is one reason Kafka works so well for event-driven architectures.

Kafka Retry Handling

Retries in Kafka are usually handled using:

  • retry topics,
  • delayed retry topics, or
  • custom consumer retry logic.

A common pattern looks like this:

  • failed events move into retry topics,
  • consumers retry later,
  • poison messages eventually move into DLQs or parking-lot topics.

This setup is powerful, but definitely more operationally complex than RabbitMQ retry routing.

Kafka gives you more flexibility.

But it also expects more architectural discipline from the team.

The Bigger Architectural Difference

Even from the code examples, the difference becomes pretty obvious.

RabbitMQ apps usually feel:

  • workflow-oriented,
  • routing-focused, and
  • delivery-centric.

Kafka apps usually feel:

  • stream-oriented,
  • event-centric, and
  • partition-aware.

Neither one is universally better.

They’re just optimized for different kinds of problems.

And that difference becomes much more important once systems start scaling and production complexity kicks in.

10. Common Mistakes Teams Make

Most production messaging issues are not really caused by RabbitMQ or Kafka.

They usually happen because of:

  • bad assumptions,
  • over-engineering, or
  • missing operational visibility.

And honestly, the same mistakes show up again and again across teams.

Using Kafka as a Task Queue

This one happens a lot.

Kafka is amazing for:

  • event streaming,
  • analytics,
  • replayability, and
  • handling huge event volumes.

But teams sometimes use it for very simple things like:

  • background jobs,
  • workflow execution, or
  • async task processing.

That usually brings in:

  • partition management,
  • retry complexity,
  • consumer coordination, and
  • extra operational overhead.

If the actual requirement is just:

“Run tasks reliably in the background”

RabbitMQ is often the cleaner and simpler solution.

Not every async workflow needs a distributed event streaming platform.

Sometimes a queue is just a queue.

Choosing Kafka Just Because It “Scales Better”

Yes, Kafka scales extremely well.

But scalability only matters when you actually need it.

A lot of systems never reach the scale where Kafka’s architecture becomes necessary.

Meanwhile, the team still has to deal with:

  • partitions,
  • retention policies,
  • lag monitoring,
  • broker management, and
  • cluster operations.

That’s a lot of complexity to carry around for no real reason.

Good architecture solves real problems — not imaginary future scale problems.

Ignoring Idempotency

Retries eventually create duplicates.

Always assume that.

This applies to both RabbitMQ and Kafka.

If consumers are not idempotent:

  • payments may run twice,
  • emails may send twice,
  • inventory may break,
  • workflows may repeat unexpectedly.

Messaging guarantees alone won’t save you here.

Applications still need:

  • deduplication logic,
  • safe retry handling, and
  • idempotent consumers.

Experienced engineers usually assume duplicate delivery will happen eventually.

Because in distributed systems, it eventually does.

Treating RabbitMQ Like Event Storage

RabbitMQ is built for message delivery.

Not long-term event retention.

Trying to build:

  • replayable event history,
  • event sourcing systems, or
  • analytics pipelines

on top of RabbitMQ usually becomes painful later.

Kafka is naturally better for those workloads.

Using the wrong abstraction eventually creates operational headaches.

Over-Partitioning Kafka

Partitions help with parallelism.

But too many partitions create their own problems:

  • rebalance overhead,
  • broker pressure,
  • operational complexity, and
  • consumer coordination costs.

More partitions do not automatically mean better performance.

Partition strategy should match:

  • throughput requirements,
  • scaling needs, and
  • ordering guarantees.

Bad partition planning becomes very hard to fix later.

Ignoring Observability

Teams generally monitor broker uptime and stop there.

But healthy messaging systems need much deeper visibility.

You usually want to monitor:

  • queue depth,
  • consumer lag,
  • retry rates,
  • DLQ growth,
  • processing latency, and
  • throughput trends.

Distributed systems rarely fail instantly.

Problems usually build slowly over time.

Without observability, teams often discover issues only after customers complain.

11. Decision Matrix

At this point, the pattern becomes pretty obvious:

RabbitMQ and Kafka solve different kinds of problems.

They are not direct replacements for each other in every scenario.

Here’s a simple decision guide.

Scenario Better Fit Why
Background job processing RabbitMQ Simpler retries and task distribution
Workflow orchestration RabbitMQ Flexible routing and operational simplicity
Notification systems RabbitMQ Easy fanout and retry handling
Payment workflows RabbitMQ Better delivery-focused control
Event streaming Kafka High-throughput distributed event log
Real-time analytics Kafka Replayability and scalable consumers
Audit systems Kafka Durable event retention
Event sourcing Kafka Immutable event history
CDC pipelines Kafka Stream-first architecture
Simple async microservice communication RabbitMQ Lower operational overhead
Large-scale event platforms Kafka Built for distributed streaming

A Practical Rule of Thumb

A simple rule usually works well:

Choose RabbitMQ when the main concern is:

  • task execution,
  • workflow coordination,
  • retries, and
  • operational simplicity.

Choose Kafka when the main concern is:

  • event streaming,
  • replayability,
  • analytics, and
  • long-term event retention.

That distinction alone clears up a lot of confusion early in system design.

Final Thoughts

RabbitMQ and Kafka are both excellent technologies and were designed with very different goals.

Good engineering is not about picking the most impressive or cutting-edge technology.

It’s about choosing the technology that fits naturally, stays maintainable, and behaves predictably under real production pressure.

Many mature systems eventually use both RabbitMQ and Kafka together.

The important part is knowing where each one actually fits best.

Appreciate your support and suggestions.

Wir haben einen Website-Crawler gebaut, der 50+ Checks in einem Scan macht — kostenlos

Das Problem: Eine Website checken = 8 Tools öffnen

Als Software-Agentur prüfen wir bei Enjyn ständig Websites. Bei Erstgesprächen mit potenziellen Kunden, bei bestehenden Projekten, vor Übergaben, nach Releases. Die Frage ist immer dieselbe: Ist diese Website technisch sauber, DSGVO-konform und sicher?

Die Antwort findet sich in keinem einzelnen Tool:

Aspekt Tool das wir bisher öffneten
Performance Google PageSpeed Insights, GTmetrix
Security Headers securityheaders.com
SSL-Zertifikat SSL Labs
SEO Ahrefs / Screaming Frog / Mangools
DSGVO-Check manuell
Cookie-Banner manuell
Schema.org Google Rich Results Test
Mobile PageSpeed Mobile

Acht Tools für einen Website-Check. Jedes mit eigener UI, eigenen Bewertungslogiken, eigenen Reports. Bei zwanzig Websites am Tag ist das ein halber Arbeitstag nur fürs Hin- und Herklicken.

Wir wollten ein Tool, das alles auf einmal macht — speziell für den DACH-Markt mit DSGVO-Fokus.

Die Frage: Können wir das selbst bauen?

Bestehende All-in-One-Tools gibt es. Aber:

  • Die meisten sind englischsprachig und ignorieren deutsche Rechtsanforderungen (Impressumspflicht §5 TMG, DSGVO-Cookie-Regeln, Google Fonts Schrems II)
  • Viele sind kostenpflichtige SaaS ab 50€/Monat
  • Die kostenlosen geben nur Teil-Reports und wollen dich in einen Funnel zwingen

Was wir wollten:

  • Eine URL rein, vollständiger Report raus
  • DSGVO-zentrisch (Impressum, Datenschutz, externe Fonts, Tracking-ohne-Consent)
  • Kostenlos, ohne Registrierung
  • Objektive Bewertung — keine subjektiven Faktoren

Also haben wir uns hingesetzt und den Enjyn Crawler gebaut. Heute prüft er über 50 Faktoren in einem einzigen Scan.

Was wir prüfen — die 50+ Checks

Vier Kategorien, jede mit konkreten technischen Tests:

Sicherheit (14 Checks)

HTTPS/SSL, HSTS Header, CSP Header, X-Frame-Options, X-Content-Type, Referrer-Policy, Permissions-Policy, security.txt, Mixed Content, CSRF-Schutz, Server-Info Leakage, PHP-Version exposed, sensible Dateien (.env, .git), WP User Enumeration.

SEO (15 Checks)

robots.txt, sitemap.xml, llms.txt, Meta-Title (10-70 Zeichen), Meta-Description (50-160 Zeichen), Canonical URL, H1-H6 Struktur, Open Graph, Twitter Cards, Schema.org, Favicon, Alt-Texte, interne/externe Links, Wortanzahl.

Rechtliches/DSGVO (12 Checks)

Impressum, Datenschutzerklärung, AGB, Widerruf, Cookie-Banner, Cookie-Richtlinie, Google Fonts (Schrems II!), Google Analytics, Facebook Pixel, Tracking ohne Consent, TMG/DDG-Prüfung, Kontaktdaten.

Performance & Mobile (13 Checks)

Ladezeit, Seitengröße, GZIP/Brotli, HTTP/2, DOM-Größe, Ressourcen-Anzahl, Bilder, Scripts, Stylesheets, Third-Party Requests, Viewport, Skip-Link, ARIA Landmarks.

Der Stack — komplett offen

Im Gegensatz zum KYC-Artikel müssen wir hier nichts verstecken — der Crawler ist Open-Approach, es gibt keinen Anti-Fraud-Grund für Geheimhaltung:

Sprache: Python 3.11+
Web-Framework: Flask
HTTP-Client: httpx (statt requests — async + HTTP/2)
HTML-Parser: BeautifulSoup4 + lxml
Spracherkennung: Lingua (AI-basiert, 95%+ Genauigkeit)
Datenbank: SQLite (Scans werden temporär gespeichert)
WSGI: Gunicorn

Bewusst kein Headless Browser (Puppeteer/Playwright). Warum? Geschwindigkeit. Ein vollständiger Scan dauert bei uns 2-8 Sekunden statt 30-60 Sekunden bei Lighthouse-basierten Tools.

Das geht, weil wir bewusst nicht jedes JavaScript ausführen. Wir analysieren das gelieferte HTML, Headers und statische Assets — das deckt 95% der relevanten Probleme ab. Die fehlenden 5% (z.B. Runtime-Performance) sind genau die, für die du PageSpeed Insights eh weiter brauchst.

Beispiel: SSL-Check

So sieht der SSL-Check unter der Haube aus:

import ssl
import socket

def check_ssl(domain):
    context = ssl.create_default_context()
    with socket.create_connection((domain, 443), timeout=5) as sock:
        with context.wrap_socket(sock, server_hostname=domain) as ssock:
            cert = ssock.getpeercert()

    # Ablaufdatum extrahieren
    not_after = cert['notAfter']
    expires = datetime.strptime(not_after, '%b %d %H:%M:%S %Y %Z')
    days_left = (expires - datetime.utcnow()).days

    if days_left < 14:
        return {'status': 'critical', 'days': days_left}
    elif days_left < 30:
        return {'status': 'warning', 'days': days_left}
    else:
        return {'status': 'ok', 'days': days_left}

Kein Hexenwerk. Nur Python-Standardbibliothek. Die meisten unserer 50+ Checks sind ähnlich straightforward — der Aufwand lag nicht im Einzelcheck, sondern im Sammeln aller Edge-Cases, die in deutschen Websites real auftreten.

Beispiel: Google Fonts Detection (Schrems II!)

Ein Check, der erst seit Schrems II 2020 wirklich wichtig ist:

def check_google_fonts(html):
    # Schrems II: Google Fonts ohne Consent = DSGVO-Verstoß
    # (LG München, Urteil vom 20.01.2022, Az. 3 O 17493/20)

    indicators = [
        'fonts.googleapis.com',
        'fonts.gstatic.com',
        '@import url\(.*fonts\.google'
    ]

    for pattern in indicators:
        if re.search(pattern, html, re.IGNORECASE):
            return {
                'status': 'critical',
                'reason': 'Google Fonts wird extern geladen',
                'risk': 'IP-Übertragung in die USA ohne Consent',
                'fix': 'Self-Hosting der Fonts oder Consent vor Laden'
            }

    return {'status': 'ok'}

Das ist ein typischer Crawler-Check, den kein internationales Tool macht, weil das Rechtsproblem deutsch-spezifisch ist.

Das Scoring: eRate + eTrust

Statt eine einzige “Note” zu geben, splitten wir das Ergebnis bewusst in zwei Bewertungen:

eRate — Technische Qualität (0-100%)

Kategorie Max. Punkte
Sicherheit 30
Datenschutz & Rechtliches 25
SEO & Auffindbarkeit 25
Performance 20

eTrust — Vertrauenswürdigkeit (0-100%)

Faktor Punkte
SSL/HTTPS 15
Impressum 15
Datenschutzerklärung 12
Kontaktinformationen 10
Keine Malware-Indikatoren 10
Sichere Formulare 8
Professionelles Erscheinungsbild 8
Transparenz 7

Warum zwei Werte? Weil eine Website technisch perfekt sein kann (eRate A+), aber unseriös wirken (kein Impressum, kein Kontakt → eTrust D). Beide Aspekte sind für Besucher relevant — und werden mit nur einem Score vermischt.

Was wir bewusst NICHT machen

Keine echte Performance-Messung im Browser. Wir messen Server-Response-Time und Asset-Größen, nicht Time-to-Interactive oder Largest Contentful Paint. Dafür gibt es PageSpeed Insights — wir wollten nicht der zweite sein, sondern die anderen 95% abdecken.

Keine Penetration Tests. Wir scannen passiv. Kein Brute-Force, kein Port-Scanning, kein SQL-Injection-Test. Das wäre ohne Erlaubnis des Website-Betreibers illegal.

Keine englischsprachigen Websites. Wir erkennen die Sprache der Website automatisch (via Lingua + HTML lang-Attribut) und führen den vollständigen Scan nur für deutschsprachige Websites durch. Internationale Seiten bekommen einen reduzierten Report.

Keine Hidden-Funnel-Logic. Du bekommst den vollen Report ohne E-Mail-Adresse, ohne Signup, ohne “Premium-Features dahinter”. Die Bewertung ist die Bewertung.

Das Widget — Trust nach außen tragen

Nach jedem Scan kannst du das eRate/eTrust-Widget kostenlos auf deine Website einbinden. Ähnlich wie Trustpilot, aber für objektive Technik-Kriterien statt subjektive Bewertungen:

<iframe 
  src="https://crawler.eg-core.de/widget/embed?domain=deinedomain.de"
  width="300" 
  height="150"
  frameborder="0">
</iframe>

Wir verifizieren beim Einbetten, dass die anzeigende Domain mit der gescannten übereinstimmt — kein “Fake-A+-Badge”, den jemand stehlen könnte.

Warum das was bringt: Ein automatisch generiertes, unbestechliches Vertrauenssiegel. Im Gegensatz zu kaufbaren Gütesiegeln bedeutet ein A+ hier real, dass dein technisches Setup sauber ist.

Wer das nutzt

Aktuell scannen wir mit dem Crawler intern:

  • Vor jedem Erstgespräch mit potenziellen Kunden — wir gehen vorbereitet ins Meeting
  • Nach jedem Projekt-Release als Quality Gate
  • Quartalsweise für alle Hosting-Kunden, die unser Audit-Paket gebucht haben

Extern wird er von Webentwicklern, Agenturen und einigen Datenschutzbeauftragten genutzt, um schnell Sites zu screenen. Genaue Zahlen behalten wir für uns — aber er läuft stabil und schnell genug für unseren Bedarf.

Wenn du es probieren willst

Wenn du das Widget auf deiner Website einbinden willst: nach erfolgreichem Scan bekommst du den Embed-Code direkt im Report.

Build Your First AI App with NVIDIA NIM in 30 Minutes

Most students I’ve taught have used ChatGPT. Far fewer have called a model from code.

That is the gap this post is meant to close. In 30 minutes, you’ll call an NVIDIA-hosted language model from Python, pass it a small knowledge base, and make it answer only from that data. No GPU setup, no CUDA detour, no pretending a notebook is production. The goal is simple — write a normal Python program that talks to an LLM and gets useful text back.

I’m B Torkian, NVIDIA Developer Champion, and I use this as a starter workshop for university and community groups. I’ve run a version of it with about 40 students. What usually surprises people is how ordinary the app feels. Most of it is normal software; one function call in the middle just happens to be weirdly powerful.

Everything runs in Google Colab because, for a room full of mixed laptops (I have made peace with this), boring setup wins.

This is Part 1 of a 5-part series that goes from one API call all the way to a small tool-using agent. Each post stands on its own, so start here and move forward as far as you want to go.

What you’re building

User question → Python app → NVIDIA NIM API → LLM response → App output

A small campus assistant. It will call an NVIDIA-hosted Llama model, use the data you provide, and refuse when the answer isn’t there.

That refusal part matters. Demos can guess. Useful apps need to know when to say “I don’t know.”

What NVIDIA NIM is

NIM stands for NVIDIA Inference Microservices. For this post, treat it as hosted model inference from NVIDIA with a clean API in front.

There are two common ways to use it:

  1. Hosted through NVIDIA’s API Catalog at build.nvidia.com. That’s what we’re using here; check the current catalog terms before you teach it, because credits and available models can change.
  2. Self-hosted on your own GPU later, with the same API shape. (That’s Part 4 of this series.)

Whoever decided NVIDIA’s API should mimic OpenAI’s saved everyone a week of onboarding. You use the client most people have already seen, point it at a different endpoint, and move on.

Prerequisites (5 minutes)

  1. A free NVIDIA Developer account — developer.nvidia.com
  2. An API key from build.nvidia.com → pick any model → Get API Key. It starts with nvapi-.
  3. A Google account for Colab.

The first time I taught this, I forgot to say the key starts with nvapi-, and half the room pasted the wrong thing (usually not their fault). Check that before you debug anything else.

Step 1: Open Colab and install the client

NVIDIA’s API Catalog is OpenAI-compatible, so we’ll use the standard openai Python client and point it at NVIDIA’s endpoint.

%pip install -q openai

import os, getpass
from openai import OpenAI

os.environ['NVIDIA_API_KEY'] = getpass.getpass('Paste your NVIDIA API key: ')

client = OpenAI(
    base_url='https://integrate.api.nvidia.com/v1',
    api_key=os.environ['NVIDIA_API_KEY'],
)

MODEL = 'meta/llama-3.1-8b-instruct'

Notice two things:

  • base_url points at NVIDIA’s hosted inference endpoint.
  • MODEL is just a model name from the API Catalog. Swap it later if you want; the call shape does not change.

Step 2: Make your first model call

def ask(system_prompt: str, user_message: str) -> str:
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {'role': 'system', 'content': system_prompt},
            {'role': 'user',   'content': user_message},
        ],
        temperature=0.3,
        max_tokens=400,
    )
    return response.choices[0].message.content

print(ask(
    system_prompt='You are a helpful, concise assistant.',
    user_message='Explain GPU acceleration to a first-year CS student in 5 sentences.',
))

Run it.

That ask() function is the basic shape of a lot of AI apps — instructions in, user input in, model response out. Real systems add plumbing, but this is the core.

Step 3: Use the system prompt to steer the model

Now keep the model and change its job description:

print(ask(
    system_prompt='You are a sarcastic but accurate professor. Keep it under 5 sentences.',
    user_message='Explain GPU acceleration to a first-year CS student.',
))

The output changes because the system prompt changes the model’s job. A little precision buys you a lot here; vague prompts make debugging miserable.

Treat prompts like tiny specs — include constraints, output shape, and what to do when a question goes off-track. Then test with slightly annoying questions, because users will absolutely ask those.

Step 4: Build the campus assistant

An LLM doesn’t know your campus schedule. It may still sound confident, which is exactly the problem.

So put the campus information directly into the prompt:

campus_info = """
The AI Club meets every Thursday at 5 PM in the engineering building, room 204.
The GPU computing lab is open Monday to Friday from 10 AM to 6 PM.
Students can join the NVIDIA Developer Program for free.
The next workshop will cover Retrieval Augmented Generation (RAG).
Office hours for the AI/ML faculty are Tuesdays 2-4 PM.
"""

assistant_system_prompt = f"""You are a campus assistant. Answer ONLY using the
information in CAMPUS INFO below. If the answer is not in there, say
"I don't have that information — check with the AI Club."

CAMPUS INFO:
{campus_info}
"""

for question in [
    'When does the AI Club meet?',
    'Is the GPU lab open on Saturday?',
    'What is the wifi password?',
]:
    print(f'Q: {question}')
    print(f'A: {ask(assistant_system_prompt, question)}n')

Run it and read the outputs before moving on. The AI Club answer should come straight from the text. For Saturday, the model often refuses with the fallback line instead of inferring closed. That is the behavior I want for this exercise — “Monday to Friday” gives a human enough to reason about Saturday, but the exact Saturday answer is not stated in the provided data.

The wifi question should also get the fallback line, because there is nothing in campus_info about passwords. If your model says “I don’t have that information — check with the AI Club,” do not treat that as a failure. It stayed inside the box we gave it, which is the whole point.

Last cohort, one student replaced the campus info with their D&D campaign notes and ended up with the most fun bug-hunting session of the day. The pattern works for silly data and useful data, which is why it sticks.

Step 5: What you actually did

You just built manual RAG — you picked the context by hand, inserted it into the prompt, and asked the model to answer from that context. In a production-ish version, the hand-picked campus_info string becomes whatever your retrieval system finds.

In a real app, the context probably comes from PDFs, docs, tickets, lecture notes, or a wiki. You retrieve a few relevant chunks at query time, usually with embeddings and a vector database, then pass only those along.

The model call barely changes — campus_info becomes the output of retrieval. Most of the engineering work lives in that swap.

That swap is exactly what Part 2 of this series is about.

Get the code

Repo: github.com/torkian/nvidia-nim-workshop
One-click Colab: Open the notebook

Next in this series

Part 2: Give Your AI App Real Knowledge — Embedding-Based RAG with NVIDIA NIM. We replace the hand-picked context string with a real retriever that uses NVIDIA’s embedding model, cosine similarity, and a query/passage distinction that most beginners get wrong on the first try.

Contributing to Laravel Maestro Starter Kits Without Losing Your Changes

What is Maestro

Maestro is an upstream generator repository for generating and managing Laravel starter kits.

The following starter kit repositories are downstream repositories generated from Maestro, and directly modifying the starter kit side does not propagate changes upstream.

  • laravel/react-starter-kit
  • laravel/vue-starter-kit
  • laravel/svelte-starter-kit

Therefore, when making changes such as the following, you must modify the maestro side instead.

  • Shared starter kit bug fixes
  • Cross-variant fixes
  • Generator-level specification changes
  • UI consistency adjustments across stacks

Maestro Structure

Unlike a normal Laravel application, Maestro has the following structure.

  • kits/ — source template
  • build/ — generated application (generated runtime)
  • watcher — synchronization layer from build/ → kits/

Inertia Variant Matrix

variant build command starter_kit
React Fortify php artisan build --kit=react react
React WorkOS php artisan build --kit=react --workos react-workos
React Fortify + Teams php artisan build --kit=react --teams react-teams
React WorkOS + Teams php artisan build --kit=react --workos --teams react-workos-teams

The same applies to Vue / Svelte.

If you misunderstand “which side is the source of truth,” unintended rollbacks and unnecessary diffs are very likely to occur.

Basic Principles

kits/ Is the Source of Truth

Any diff that should be committed must exist in kits/. build/ is generated output and should be treated as a working directory for verification, temporary edits, and runtime testing.

You must not commit diffs from the build/ side.

build/ Is Disposable

Treat build/ as something that can always be regenerated. Since its contents change significantly whenever variants are switched, do not place long-term changes, source patches, or persistent fixes directly in build/. Permanent changes belong in kits/.

composer kit:run Is Convenient but Also Dangerous

composer kit:run is a convenience command that launches setup, the Laravel dev server, the Vite dev server, and the watcher together.

However, the watcher treats build/ as authoritative and writes changes back into kits/. As a result, if you start kit:run using an outdated build/, changes in kits/ may get rolled back.

After directly editing kits/, never run composer kit:run with an outdated build/. Always rebuild first.

Initial Setup

git clone https://github.com/laravel/maestro.git
cd maestro/orchestrator

composer install
npm install

Building a Starter Kit

At the beginning of work, expand the target variant into build/.

cd orchestrator

php artisan build --kit=react
php artisan build --kit=vue
php artisan build --kit=svelte

php artisan build --kit=react --workos
php artisan build --kit=react --teams

The current build target is stored at:

orchestrator/storage/app/private/starter_kit

Starting the Development Server

Do not start directly from build/. Instead, run the following from orchestrator/.

composer kit:run

This command performs the following together:

  • composer setup in build/
  • Starts the Laravel dev server
  • Starts the Vite dev server
  • Starts the watcher

Typical endpoints:

  • Laravel: http://localhost:8000
  • Vite: http://localhost:5173

Safe Workflow

Pattern 1: Edit on the build Side (for temporary verification)

build → kit:run → edit build → verify behavior

Convenient for exploratory UI adjustments. However, since build/ is disposable, it is safer to reorganize the final source patch into kits/.

Pattern 2: Edit on the kits Side (recommended path for PRs)

edit kits → php artisan build ... → composer kit:run → verify

This is the recommended approach when preparing PR patches.

Switching Variants

Do not incrementally update an existing build/. Rebuild every time.

php artisan build --kit=react
php artisan build --kit=vue
php artisan build --kit=svelte

After switching, rerun composer kit:run each time for verification.

Verification Notes

DB Initialization

composer setup only runs up to php artisan migrate --force. If seeded data is required, run the following inside build/.

php artisan migrate:fresh --seed

Removing Stale Files from the Watcher

When switching variants, the watcher may determine some source assets are stale and delete them.

Examples:

  • kits/Inertia/Fortify/React/chisel-paths.php
  • kits/Inertia/Fortify/Vue/chisel-paths.php
  • kits/Inertia/Fortify/Svelte/chisel-paths.php

Handle these separately from the main change set.

Example Routes for Verifying UI Copy Changes

For UI text changes in the Fortify variant, the following routes are usually sufficient to verify the primary flows.

  • /login
  • /user/confirm-password
  • /verify-email
  • /settings/security
  • /settings/appearance
  • /settings/profile

Since WorkOS has a higher verification cost, prioritizing Fortify variants is acceptable for minor changes.

How to Think About Staging

If the PR’s main purpose is a UI copy change, then only stage the text-related diffs.

The following should generally be separated:

  • chisel-paths.php deleted by the watcher
  • Temporary fixes in build/
  • Assertions for browser tests
  • Unrelated runtime bug fixes
# Instead of git add ., explicitly add target files
git add kits/path/to/changed-file.blade.php

Pre-PR Checklist

  • [ ] Are there any staged diffs from build/?
  • [ ] Are stale deletions from the watcher mixed in?
  • [ ] Are there unnecessary cross-variant diffs?
  • [ ] Are temporary browser test changes still present?
  • [ ] Did you check git diff --cached?
  • [ ] Is the source of truth correctly on the kits/ side?

Summary

If you handle Maestro like a normal Laravel app, diff rollbacks and unnecessary commits become very easy to trigger. To create PRs safely, maintain the following sequence.

Safe PR Flow

  1. Apply changes in kits/
  2. Rebuild with php artisan build ...
  3. Verify with composer kit:run
  4. Stage only intended diffs
  5. Separate watcher/browser-test side effects

Git Advanced: The Commands I Wish I Knew Earlier (2026)

Git Advanced: The Commands I Wish I Knew Earlier (2026)

Beyond add, commit, push. These are the Git commands that actually save time.

The Basics (Quick Refresher)

git status          # What changed?
git diff             # What exactly changed?
git log --oneline   # Recent commits
git add -A           # Stage everything
git commit -m "msg"  # Commit staged changes
git push origin main # Send to remote

You know these. Let’s go further.

Undo Everything (Safely)

# Unstage a file (keep changes)
git reset HEAD path/to/file

# Discard all unstaged changes in a file
git checkout -- path/to/file
# Or newer syntax:
git restore path/to/file

# Discard ALL unstaged changes
git checkout -- .
git restore .

# Uncommit (keep changes, remove commit)
git reset --soft HEAD~1    # Changes stay staged
git reset HEAD~1           # Changes stay unstaged
git reset --hard HEAD~1    # Changes GONE forever ⚠️

# Amend last commit (fix message or add forgotten file)
git commit --amend -m "Better message"
git add forgotten-file.txt
git commit --amend         # Add file to previous commit

Interactive Rebase: Clean Up History

# Edit/reorder/squash last 5 commits
git rebase -i HEAD~5

# Opens editor with:
pick abc1234 First commit
pick def5678 Second commit
pick ghi9012 Third commit
pick jkl3456 Fourth commit
pick mno7890 Fifth commit

# Change 'pick' to:
# reword  = edit commit message
# edit    = pause to amend files
# squash  = merge into previous commit
# fixup   = merge into previous (discard this message)
# drop    = remove commit entirely
# exec    = run shell command

# Example: Squash the last 2 commits into one
pick abc1234 First commit
squash def5678 Second commit
# → Creates single commit with combined changes

Stashing: Save Work Without Committing

# Save current work temporarily
git stash
git stash push -m "WIP: feature X"

# List all stashes
git stash list
# stash@{0}: On main: WIP: feature X
# stash@{1}: On main: bugfix attempt

# Restore most recent stash
git stash pop      # Apply + remove from list
git stash apply    # Apply + keep in list

# Restore specific stash
git stash apply stash@{1}

# Drop a stash
git stash drop stash@{0}

# Clear all stashes
git stash clear

# Stash only some files (not everything)
git stash push -m "partial" src/utils.js src/config.js

# Stash with untracked files included
git stash push -u -m "include new files"

Searching Through History

# Search commit messages
git log --grep="bug fix" --oneline
git log --grep="auth" --since="2026-01-01"

# Search code content across all history
git log -S "functionName" --oneline     # Added/removed this string
git log -G "pattern" --oneline          # Changed matching this regex
git log -p -S "TODO"                    # Show actual diffs too

# Find which commit introduced a bug
git bisect start
git bisect bad                          # Current version has bug
git bisect good v1.0.0                  # This version was fine
# Git will check out middle commit → you test → mark good/bad
# Repeat until it finds the exact bad commit
git bisect reset                        # When done

# Search current working tree
git grep "search term"                  # In tracked files
git grep "search term" $(git rev-list --all)  # Across ALL history!

Branching Strategies That Work

Feature Branch Workflow

# Start a new feature
git checkout -b feature/user-auth
# Or:
git switch -c feature/user-auth

# Make commits...
git add -A && git commit -m "Add login page"

# Update your branch with latest main
git fetch origin
git rebase origin/main        # Cleaner than merge for PRs

# Push branch (first time needs -u)
git push -u origin feature/user-auth

# After PR is merged, clean up
git branch -d feature/user-auth              # Delete local
git push origin --delete feature/user-auth   # Delete remote

Quick Branch Management

# See all branches (local + remote)
git branch -a

# Rename current branch
git branch -m old-name new-name

# Rename remote branch (after local rename)
git push origin :old-name new-name

# Switch to previous branch
git checkout -
# Or: git switch -

# Compare branches
git log main..feature-branch --oneline    # Commits in feature but not main
git diff main...feature-branch            # Diff between branch tips

# Merge vs Rebase summary:
# git merge feature   → Creates merge commit, preserves exact history
# git rebase feature  → Linear history, rewrites commits (cleaner for PRs)

Dealing with Remote Changes

# Fetch all remotes (doesn't merge)
git fetch --all

# Pull = fetch + merge
git pull origin main

# Pull with rebase (avoids extra merge commits)
git pull --rebase origin main

# Force push (⚠️ dangerous on shared branches!)
git push --force-with-lease origin feature-branch
# Safer than --force: checks if remote changed first

# Remove remote tracking branch that was deleted
git prune
git fetch --prune                         # Fetch + cleanup stale refs

Cherry-Picking: Grab Specific Commits

# Apply one commit from another branch
git cherry-pick abc1234

# Apply multiple commits
git cherry-pick abc1234 def5678 ghi9012

# Cherry-pick without committing (edit first)
git cherry-pick -n abc1234
# Make changes, then:
git commit -m "Modified cherry-pick"

# Cherry-pick from another repo (as patch)
git format-patch -1 abc1234 --stdout > /tmp/patch.git
git am < /tmp/patch.git

Fixing Mistakes in Committed Code

# Forgot to include a file in last commit?
git add forgotten-file.js
git commit --amend --no-edit

# Need to change an older commit? Use interactive rebase:
git rebase -i HEAD~5
# Change 'pick' to 'edit' on the target commit
# Make your changes
git add fixed-file.js
git rebase --continue

# Split a commit into two:
git rebase -i HEAD~3
# Change 'pick' to 'edit' on target commit
git reset HEAD~1                # Uncommit while keeping changes
git add file-a.js               # Stage part of changes
git commit -m "Part A: file A"
git add file-b.js               # Stage rest
git commit -m "Part B: file B"
git rebase --continue

Understanding .gitignore

# Patterns
node_modules/                   # Directory
*.log                           # All .log files
!.important.log                 # Except this one
build/                          # Build output
.env                            # Secrets (NEVER commit!)
.DS_Store                       # macOS junk
.vscode/                        # Editor config (optional)

# Global ignore (for all repos)
git config --global core.excludesfile ~/.gitignore_global

# Debug what's being ignored
git check-ignore -v node_modules/debug.log
git status --ignored            # Show ignored files too

# Track a file that's already .gitignored
git add -f important.log        # Force add (-f overrides ignore rules)

# Stop tracking a file (but keep locally)
git rm --cached large-file.dat
# Then add to .gitignore

Useful Aliases

# Save typing with aliases
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
git config --global alias.lg "log --graph --oneline --decorate -20"
git config --global alias.unstage "reset HEAD --"
git config --global alias.last "log -1 --stat"
git config --global alias.amend "commit --amend --no-edit"
git config --global alias.cleanup "!git branch --merged | grep -v '\*\|main\|develop' | xargs git branch -d"
git config --global alias.discard "checkout --"

# Now use:
git co feature-branch       # = git checkout
git lg                      # Beautiful graph log
git cleanup                 # Delete merged branches
git discard filename        # Discard changes to file

Quick Reference Card

Task Command
Undo last commit (keep files) git reset --soft HEAD~1
Undo last commit (lose files) git reset --hard HEAD~1
Amend last commit git commit --amend
Save work temporarily git stash / git stash pop
Edit recent commits git rebase -i HEAD~N
Find commit that added text git log -S "text"
Compare branches git diff main...feature
Clean merged branches `git branch –merged
Safe force push {% raw %}git push --force-with-lease
Grab commit from other branch git cherry-pick <sha>
Bisect to find bug commit git bisect start/good/bad/reset

Which Git command saved you the most time?

Follow @armorbreak for more practical developer guides.

Warum wir eine eigene KYC-API gebaut haben — und es kostenlos rausgeben

Das Problem: KYC ist teuer. Sehr teuer.

Vor knapp einem Jahr hatten wir bei Enjyn ein wiederkehrendes Muster: Für mehrere unserer Kundenprojekte brauchten wir eine zuverlässige Identitätsprüfung — mal für Altersverifikation, mal für eine Community-Plattform, mal für einen Marktplatz mit Verkäufer-KYC.

Wir haben uns die üblichen Verdächtigen angeschaut:

Anbieter Preis pro Verifikation Setup
Onfido ab ~2 € Enterprise-Sales
IDnow ab ~2–5 € Vertrieblicher Erstkontakt
Veriff ab ~1–3 € Self-Service, aber Mindestvolumen

Bei einem unserer Projekte mit erwarteten 1.500 Verifikationen im Monat wären das 2.250 – 7.500 € monatlich gewesen. Für ein Tool, das vor allem im Hintergrund läuft und „nur” prüft, ob ein Personalausweis echt ist.

Die ehrliche Erkenntnis: Wir wollten das nicht zahlen. Und unsere Kunden auch nicht.

Die Frage: Können wir das selbst bauen?

Ein paar Wochenenden Recherche später war klar: Ja, können wir. Die Bestandteile sind technisch alle vorhanden:

  • Dokumenten-OCR für Ausweisdaten (Name, Geburtsdatum, MRZ-Zone)
  • Dokumenten-Authentizitätsprüfung (Hologramme, Sicherheitsmerkmale, MRZ-Checksums)
  • Datenabgleich zwischen Nutzer-Eingabe und Ausweisdaten
  • Auto-Datenextraktion aus Vorder- und Rückseite

Was wir nicht wollten:

  • Daten in die USA schicken (DSGVO-Albträume mit Schrems II)
  • Cloud-Lösungen mit Per-Call-Pricing (genau das Problem, das wir lösen wollten)
  • Black-Box-Modelle, deren Funktion wir nicht erklären können

Also haben wir uns hingesetzt und unsere eigene KI/ML-Pipeline gebaut. Komplett in-house, auf eigenen Servern in Deutschland.

Der Stack — grob umrissen

Aus naheliegenden Gründen veröffentlichen wir nicht jedes Detail (Anti-Fraud-Schutz), aber das große Bild:

Bildverarbeitung: Ausweis-Bilder werden zuerst normalisiert, entzerrt und auf Qualität geprüft. Zu unscharf, zu schräg oder Bildschirm-Fotografie → automatische Ablehnung.

OCR & MRZ-Parsing: Die Maschinenlesbare Zone (MRZ) deutscher Ausweise und Reisepässe wird ausgelesen, validiert (Prüfsummen) und mit den OCR-Daten aus dem visuellen Bereich abgeglichen.

Datenabgleich: Die vom Anwender vorab angegebenen Daten (Vorname, Nachname, Geburtsdatum) werden mit den extrahierten Ausweisdaten verglichen. Diskrepanzen → Status failed.

Auto-Löschung: Hochgeladene Bilder werden nach 10 Minuten gelöscht, Satus-Daten nach 30 Tagen. Mehr nicht.

Wie die API aussieht

Wir nutzen einen einfachen, dreistufigen Flow — bewusst minimalistisch gehalten:

1. Verifikations-Session erstellen

curl -X POST https://api.verify.enjyn.de/api/v1/verify 
  -H "Content-Type: application/json" 
  -H "X-API-Key: YOUR_API_KEY" 
  -d '{
    "vorname": "Max",
    "nachname": "Mustermann",
    "geburtsdatum": "01.01.1990",
    "return_url": "https://ihre-website.de/callback.html"
  }'

Response:

{
  "success": true,
  "session_id": "uuid",
  "token": "secure-token",
  "verify_url": "https://api.verify.enjyn.de/verify/token",
  "qr_url": "https://api.verify.enjyn.de/api/v1/qr-code/token"
}

Du leitest den Nutzer auf die verify_url weiter — oder zeigst den QR-Code an, damit er die Verifikation auf dem Smartphone fortsetzt. Der Nutzer lädt dort Vorder- und Rückseite seines Ausweises hoch, der Rest passiert automatisch.

2. Status nach Callback abrufen

Nach Abschluss leiten wir den Nutzer auf deine return_url weiter. Wichtig: Der status-Parameter in der URL ist nicht vertrauenswürdig (kann manipuliert werden). Den echten Status holst du dir per API:

curl https://api.verify.enjyn.de/api/v1/get-result/{session_id} 
  -H "X-API-Key: YOUR_API_KEY"

Response bei Erfolg:

{
  "success": true,
  "status": "verified",
  "vorname": "Max",
  "nachname": "Mustermann",
  "geburtsdatum": "01.01.1990",
  "verified_at": "2026-05-21T23:42:18Z"
}

3. Vollständige JavaScript-Integration

// 1. Session erstellen
const response = await fetch('https://api.verify.enjyn.de/api/v1/verify', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-API-Key': 'YOUR_API_KEY'
    },
    body: JSON.stringify({
        vorname: 'Max',
        nachname: 'Mustermann',
        geburtsdatum: '01.01.1990',
        return_url: 'https://ihre-website.de/callback.html'
    })
});

const { verify_url, session_id } = await response.json();

// Session-ID lokal merken für späteren Abruf
sessionStorage.setItem('kyc_session', session_id);

// 2. Nutzer zur Verifikation weiterleiten
window.location.href = verify_url;

// 3. Auf der Callback-Seite: Status SICHER validieren
const sessionId = sessionStorage.getItem('kyc_session');
const result = await fetch(
    `https://api.verify.enjyn.de/api/v1/get-result/${sessionId}`,
    { headers: { 'X-API-Key': 'YOUR_API_KEY' } }
);
const verification = await result.json();

if (verification.status === 'verified') {
    console.log('✓ Verifiziert:', verification.vorname);
}

Status-Werte

Status Beschreibung
pending Wartet auf Nutzer-Upload
processing Bilder werden verarbeitet
verified ✅ Erfolgreich
failed ❌ Fehlgeschlagen

Was wir bewusst NICHT machen

Keine internationalen Ausweise (noch nicht). Wir prüfen aktuell deutsche Personalausweise und Reisepässe. Das deckt unseren Use Case ab. Internationale Dokumente erfordern Trainingsdaten und Validierung, die wir bisher nicht haben.

Kein BaFin-Level KYC. Für Banken und Finanzdienstleister gibt es regulatorische Anforderungen (qualifizierte elektronische Signatur, Video-Ident mit Schulungsanforderungen), die wir nicht abdecken. Wenn du als Bank KYC brauchst: nimm IDnow oder PostIdent. Wir sind die richtige Wahl für E-Commerce, Communities, SaaS, Events und Marktplätze.

Kein Speichern der Originaldaten über Wochen. Manche Anbieter behalten Dokumente monatelang für „Audit-Zwecke”. Wir nicht. Bilder weg nach 10min, Session (anonymisiert) weg nach 30 Tagen.

Warum wir es kostenlos rausgeben

Faire Frage. Antwort in drei Teilen:

Erstens — Eigeninteresse. Wir nutzen es selbst in mehreren Projekten. Es kostenlos verfügbar zu machen, kostet uns kaum mehr als es nur intern zu nutzen. Die Infrastruktur läuft sowieso.

Zweitens — Marktposition. Wir sind eine Software-Agentur. Kunden, die unser KYC-API ausprobieren und damit zufrieden sind, fragen uns irgendwann auch für andere Dinge an. Hosting, Custom-Entwicklung, SEO. Das funktioniert.

Drittens — wir können Volumen-Preise später noch differenzieren. Aktuell ist es bis zu 150 Verifys (nur erfolgreich) Monatlich kostenlos.

Aktuelle Nutzung

Wir haben aktuell 8 aktive Partner, die zusammen 60–150 Verifikationen pro Monat durchführen. Klein, aber stetig wachsend. Wir wollten erst sicher sein, dass die Pipeline robust läuft, bevor wir lauter werden.

Das hier ist der Anfang davon, lauter zu werden.

Wenn du es probieren willst

  • Demo: enjyn.de/kyc — du kannst dich live durchprüfen lassen
  • API-Docs: enjyn.de/apis?kyc
  • API-Key anfordern: Kurze Nachricht über enjyn.de/kontakt mit deinem Use Case. Wir machen das aktuell noch manuell, um Missbrauch zu verhindern. Antwortzeit meist unter 6h.

The Scoped Singleton DI Bug Your AI Just Suggested

The Scoped→Singleton DI bug your AI just suggested (and how to catch it)

Of all the bugs that ship to production silently, the captured-dependency lifetime bug is one of the most expensive. It compiles. It passes your tests. It runs fine in dev. Then in production, under load, it starts corrupting data across requests. And AI assistants suggest it constantly. Here’s why — and the one Cursor rule that catches it before merge.

The bug, in 30 lines

“
You ask Cursor to add caching to OrderService. It gives you this:

`plaintext

`
// OrderService.cs
public class OrderService : IOrderService
{
private readonly IMemoryCache _cache;
private readonly OrderDbContext _db;

public OrderService(IMemoryCache cache, OrderDbContext db)
{
    _cache = cache;
    _db = db;
}

public async Task<Order?> GetAsync(int id, CancellationToken ct)
{
    if (_cache.TryGetValue(id, out Order? cached)) return cached;

    var order = await _db.Orders.FindAsync(new object[] { id }, ct);
    if (order is not null) _cache.Set(id, order, TimeSpan.FromMinutes(5));
    return order;
}

}

// Program.cs
builder.Services.AddDbContext(…);
builder.Services.AddScoped();
builder.Services.AddMemoryCache(); // ← registers IMemoryCache as Singleton

Looks correct. Compiles. Tests pass. Shipped.

What actually happens at runtime

********
IMemoryCache is registered as Singleton — one instance for the entire app’s lifetime. OrderService is registered as Scoped — one instance per HTTP request.

plaintext
On its own, that’s fine. The problem is what you cached: an Order entity, which is in turn attached to OrderDbContext — also Scoped. The cache, alive for the lifetime of the application, now holds a reference to an entity attached to a DbContext that was disposed when the original request ended.

“
Now request #2 comes in. It hits the cache, gets the order, mutates a property. Then request #3 hits the cache, sees the mutation, and decides to write something else based on it. Then request #4 wakes up the entity’s dispose-tracking and explodes with ObjectDisposedException — but only sometimes, depending on the GC pressure that day.

Welcome to the longest debugging session of your year.

Why AI assistants suggest this constantly

The patterns the AI has seen most often in its training data — short examples, blog tutorials, StackOverflow answers — almost always omit DI registration. A typical “caching with IMemoryCache” snippet looks like ten lines, with no reference to where the service is registered or with what lifetime.

The AI learned the surface pattern (“inject IMemoryCache, call .Set”) without the surrounding constraint (“…unless the consumer is Scoped and the cached value graph reaches into Scoped infrastructure”). When you ask it to add caching to your codebase, it pattern-matches against the surface form. The constraint is invisible to it.

This isn’t a “the AI is dumb” critique. Most senior developers ship this exact bug at least once. The patterns in the wild teach the wrong lesson.

The five lifetime traps to teach the AI

If you’re going to enforce one set of rules on AI-suggested .NET code, make it these:

  1. Scoped or Transient injected into Singleton

“
The classic. A Singleton constructor takes IRepository (Scoped). The Singleton captures it forever. Requests share state. Data corrupts.

The rule: when adding a constructor parameter, check the parameter type’s registered lifetime. If the consumer is Singleton and the parameter is Scoped/Transient, refuse and surface the issue.

  1. DbContext captured by anything Singleton

****
Special case of #1 but worth its own callout. DbContext is always Scoped — it has to be, it tracks per-request state. Any Singleton that captures a DbContext is a bug. If you need DB access from a Singleton, inject IServiceScopeFactory and create a scope per operation.

  1. Cached entities still attached to a DbContext

The bug from the example. The cache outlives the DbContext, but holds a graph that depends on it.

****“
The rule: what goes into long-lived caches must be either (a) AsNoTracking()’d, (b) projected to a DTO, or (c) detached explicitly.

4. HttpClient instantiated with new


A long-running app that does new HttpClient() on every call leaks sockets — eventually exhausting the connection pool. Even worse: a Singleton that captures a single HttpClient reuses DNS forever.

The rule: always inject IHttpClientFactory and call CreateClient(name). Never new HttpClient() outside of one-shot scripts.


### 
5. Hosted services touching Scoped dependencies directly


``
IHostedService is Singleton-by-construction. Inject a Scoped repo into one and it'll be alive for the lifetime of the process — every "scoped" operation will share state. Worse, the DbContext will leak.


****``````


The rule: in any BackgroundService or IHostedService, never inject Scoped dependencies directly. Inject IServiceScopeFactory and create a scope per unit of work.


## 
The Cursor rule that catches all five


``[](https://agenticstandardcontact-byte.github.io/agentic-architect/)``

````
The dotnet-di.mdc rule in Agentic Architect codifies the above. When Cursor is editing a file where DI is happening — Program.cs, Startup.cs, ServiceCollectionExtensions.cs, any class constructor — the rule activates and audits suggestions for:


- Lifetime mismatches between consumer and constructor parameters

- Captured Scoped dependencies inside hosted services or background workers

- ``

Direct HttpClient instantiation

- Captured tracked entities in long-lived caches

- Static helpers reaching into scoped infrastructure



**
The trick is the scoping: it loads only on files where DI is actually happening — not on every prompt. Your token budget stays sane. The AI stays sharp on the file you're actually in.


## 
The bigger pattern: enforce, don't suggest


********
The reframe that took me a year of using AI assistants to internalize is this: generic prompts ask the AI to suggest good patterns. Scoped rules force it to enforce them.


"Be careful with DI lifetimes" is a suggestion. The AI will agree, nod sagely, then ship the captured-Scoped bug an hour later when you're tired.


"Before suggesting any constructor change, audit the lifetime contract" is a rule. The AI now has a checklist. It pauses, runs the check, and either suggests a boundary-respecting alternative or asks you a targeted question — instead of confidently shipping the bug.


**
The first time the AI catches a Scoped-into-Singleton in code you wrote, the kit pays for itself.

---

*Originally published at [https://agenticstandardcontact-byte.github.io/agentic-architect/blog/02-scoped-singleton-di-bug.html](https://agenticstandardcontact-byte.github.io/agentic-architect/blog/02-scoped-singleton-di-bug.html). Part of the [Agentic Architect](https://agenticstandardcontact-byte.github.io/agentic-architect/) persistence kit for Cursor + .NET.*