SlideShare a Scribd company logo
Speaker: Alexey Golub @Tyrrrz
Dependency absolution
Application as a pipeline
Who am I?
Speaker: Alexey Golub @Tyrrrz
Open source developer
• https://github.com/Tyrrrz
• 6 active projects
• 4000 total stars
• 400k total downloads
Senior software developer
• Svitla Systems, Inc
• C#, ASP.NET Core, Docker, AWS
Interests
• C#, F#, JavaScript
• Functional programming
• Language design, parsing, DSLs
• Product design, photography
Best code is no code at all
Bad design decisions lead to writing more code in the long run
Speaker: Alexey Golub @Tyrrrz
Domain modeling
Speaker: Alexey Golub @Tyrrrz
Object-oriented approach
Speaker: Alexey Golub @Tyrrrz
public class Ledger
{
public Guid Id { get; }
public decimal Balance { get; private set; }
public Ledger(Guid id, decimal initialBalance) { /* ... */ }
public void Debit(decimal amount) { /* ... */ }
public void Credit(decimal amount) { /* ... */ }
public static Ledger Create(decimal initialBalance = 0.0M) { /* ... */ }
public static Ledger Retrieve(Guid id) { /* ... */ }
}
public void Debit(decimal amount)
{
if (Balance < amount)
throw new InsufficientFundsException();
Balance -= amount;
Database.Save(this);
}
Speaker: Alexey Golub @Tyrrrz
Business logic
Dependency
Speaker: Alexey Golub @Tyrrrz
Object
Dependency
Speaker: Alexey Golub @Tyrrrz
Object
Dependency
Object
Abstraction
Dependency
Mock
public void Debit(decimal amount)
{
if (Balance < amount)
throw new InsufficientFundsException();
Balance -= amount;
ServiceLocator.GetService<IDatabase>().Save(this);
}
Speaker: Alexey Golub @Tyrrrz
public void Debit(decimal amount)
{
if (Balance < amount)
throw new InsufficientFundsException();
Balance -= amount;
ServiceLocator.GetService<IDatabase>().Save(this);
}
private readonly IDatabase _database;
/* ... */
public void Debit(decimal amount)
{
if (Balance < amount)
throw new InsufficientFundsException();
Balance -= amount;
_database.Save(this);
}
Speaker: Alexey Golub @Tyrrrz
Abstract dependency
Benefits of dependency injection
• Inversion of control
• Lifetime management
• Decoupled implementations
• Reduced module complexity
• Isolated testing
Speaker: Alexey Golub @Tyrrrz
Layered architecture
Speaker: Alexey Golub @Tyrrrz
Speaker: Alexey Golub @Tyrrrz
Domain models
Data access
Services
System boundary
Layered/onion architecture
public class Ledger
{
public Guid Id { get; set; }
public decimal Amount { get; set; }
}
public interface ILedgerRepository
{
Task AddAsync(Ledger ledger);
Task UpdateAsync(Ledger ledger);
Task<Ledger> RetrieveAsync(Guid id);
}
public interface ILedgerService
{
Task<Ledger> CreateAsync(decimal initialBalance = 0.0M);
Task<Ledger> RetrieveAsync(Guid id);
Task DebitAsync(Guid id, decimal amount);
Task CreditAsync(Guid id, decimal amount);
}
Speaker: Alexey Golub @Tyrrrz
Speaker: Alexey Golub @Tyrrrz
Ledger
LedgerRepository
LedgerService
LedgerController
System boundary (entry point)
Service layer
Data access layer
Domain model
Speaker: Alexey Golub @Tyrrrz
Ledger
LedgerRepository
LedgerService
LedgerController
TransactionFee Counterparty
TransactionController
TransactionService
FeeService
Statement
StatementService
CounterpartyService
FeeRepository TransactionRepositoryCounterpartyRepository StatementRepository
ScheduledJob
NotificationService
Layers of issues
Speaker: Alexey Golub @Tyrrrz
Hard to combine with OOD
• Existing paradigms became anti-patterns
• Encapsulation is gone
• Inheritance is disfavored
• Static classes & methods are “untestable”
Speaker: Alexey Golub @Tyrrrz
Causational indirectness
• Hard to trace
• Hard to reason about
• Implicit dependencies & DI made us lazy
Speaker: Alexey Golub @Tyrrrz
Leaky async
• Async stems from IO side-effects and leaks into business logic
• Unnecessary state machines impact performance
• Code becomes redundantly verbose
Speaker: Alexey Golub @Tyrrrz
public class LedgerService : ILedgerService
{
private readonly ILedgerRepository _repository;
/* ... */
public async Task DebitAsync(Guid ledgerId, decimal amount)
{
var ledger = await _repository.GetByIdAsync(ledgerId);
if (ledger is null)
throw new EntityNotFoundException();
if (ledger.Balance < amount)
throw new InsufficientFundsException();
ledger.Balance -= amount;
await _repository.SaveChangesAsync();
}
}
Speaker: Alexey Golub @Tyrrrz
Leaky async
public interface ILedgerService
{
Task<Ledger> CreateAsync(decimal initialBalance = 0.0M);
Task<Ledger> RetrieveAsync(Guid id);
Task DebitAsync(Guid id, decimal amount);
Task CreditAsync(Guid id, decimal amount);
}
Speaker: Alexey Golub @Tyrrrz
ILedgerService reveals the fact that LedgerService depends on ILedgerRepository
Obscured complexity
• Module complexity is reduced
• Total complexity is increased
• Assumptions between communicating modules
Speaker: Alexey Golub @Tyrrrz
Mock-based testing
• Implementation-aware
• Very brittle
• Massive time sink
Speaker: Alexey Golub @Tyrrrz
// Arrange
var transactionRepositoryMock = new Mock<ITransactionRepository>();
transactionRepositoryMock.Setup(x => x.GetAll())
.Returns(testData.AsQueryable());
var counterpartyServiceMock = new Mock<ICounterpartyService>();
counterpartyServiceMock.Setup(x => x.GetCounterpartyAsync(It.IsAny<Transaction>()))
.ReturnsAsync(testCounterparty);
var transactionService = new TransactionService(
transactionRepositoryMock.Object,
counterpartyServiceMock.Object
);
var transaction = new Transaction
{
// ...
}
// Act
await transactionService.ExecuteTransactionAsync(transaction);
// Assert
// ...
Speaker: Alexey Golub @Tyrrrz
Implementation-aware
Autotelic abstractions
• Every object requires an explicit abstraction
• Abstractions are needed for the sole purpose of mocking
• Abstractions don’t try to encapsulate behavior
• Abstractions are owned by implementations instead of consumers
Speaker: Alexey Golub @Tyrrrz
Abstraction is a great tool
and a terrible goal
Speaker: Alexey Golub @Tyrrrz
Is OOP the wrong tool for
the job?
Speaker: Alexey Golub @Tyrrrz
Functional architecture
Speaker: Alexey Golub @Tyrrrz
ImpurePure
Speaker: Alexey Golub @Tyrrrz
Side-effects
Data
transformation
Speaker: Alexey Golub @Tyrrrz
Validating, parsing,
mapping, ordering,
filtering, projecting…
Reading request, writing
to DB, enqueuing
messages…
ImpurePure
Speaker: Alexey Golub @Tyrrrz
ImpurePure
Speaker: Alexey Golub @Tyrrrz
Pure functions
Speaker: Alexey Golub @Tyrrrz
Speaker: Alexey Golub @Tyrrrz
F(x)Data in Data out
Isolation
SQL
Stateless
Speaker: Alexey Golub @Tyrrrz
Speaker: Alexey Golub @Tyrrrz
var fees = _invoiceService.CalculateFees();
Can I make any assumptions about _invoiceService?
Speaker: Alexey Golub @Tyrrrz
var fees = InvoiceLogic.CalculateFees(ledger, invoiceDate);
Dependencies are clear which makes the intent clear
Data-driven
Speaker: Alexey Golub @Tyrrrz
private readonly CommonDbContext _dbContext;
/* ... */
public IEnumerable<Transaction> GetCompletedTransactions(Guid ledgerId)
{
return _dbContext.Transactions
.Where(t => t.LedgerId == ledgerId)
.Where(t => t.IsCompleted);
}
Speaker: Alexey Golub @Tyrrrz
Data container
Speaker: Alexey Golub @Tyrrrz
public static IEnumerable<Transaction> GetCompletedTransactions(
IEnumerable<Transaction> allTransactions, Guid ledgerId)
{
return allTransactions
.Where(t => t.LedgerId == ledgerId)
.Where(t => t.IsCompleted);
}
Deterministic
Speaker: Alexey Golub @Tyrrrz
public static DateTimeOffset GetInvoiceDate(int day)
{
var instant = DateTimeOffset.Now;
var potentialInvoiceDate = instant.Day >= day
? instant.AddDays(day - instant.Day)
: instant.AddMonths(-1).AddDays(day - instant.Day);
if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Saturday)
potentialInvoiceDate = potentialInvoiceDate.AddDays(-1);
else if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Sunday)
potentialInvoiceDate = potentialInvoiceDate.AddDays(-2);
return potentialInvoiceDate;
}
Non-deterministic
Speaker: Alexey Golub @Tyrrrz
public static DateTimeOffset GetInvoiceDate(DateTimeOffset instant, int day)
{
var potentialInvoiceDate = instant.Day >= day
? instant.AddDays(day - instant.Day)
: instant.AddMonths(-1).AddDays(day - instant.Day);
if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Saturday)
potentialInvoiceDate = potentialInvoiceDate.AddDays(-1);
else if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Sunday)
potentialInvoiceDate = potentialInvoiceDate.AddDays(-2);
return potentialInvoiceDate;
}
Speaker: Alexey Golub @Tyrrrz
Inherently testable
Speaker: Alexey Golub @Tyrrrz
var result = InvoiceLogic.GetInvoiceDate(instant, day);
Speaker: Alexey Golub @Tyrrrz
private static IEnumerable<TestCaseData> GetTestCases()
{
yield return new TestCaseData(
new DateTimeOffset(2019, 12, 05, 00, 00, 00, TimeSpan.Zero),
20,
new DateTimeOffset(2019, 11, 20, 00, 00, 00, TimeSpan.Zero)
);
yield return new TestCaseData(
new DateTimeOffset(2019, 12, 25, 00, 00, 00, TimeSpan.Zero),
22,
new DateTimeOffset(2019, 12, 20, 00, 00, 00, TimeSpan.Zero)
);
}
[Test]
[TestCaseSource(nameof(GetTestCases))]
public void GetInvoiceDate_Test(DateTimeOffset instant, int day, DateTimeOffset expectedResult)
{
// Act
var actualResult = InvoiceLogic.GetInvoiceDate(instant, day);
// Assert
Assert.That(actualResult, Is.EqualTo(expectedResult));
}
Speaker: Alexey Golub @Tyrrrz
Pure-impure segregation
Speaker: Alexey Golub @Tyrrrz
public static class Program
{
public static void Main(string[] args)
{
var max = int.Parse(Console.ReadLine());
for (var i = 1; i <= max; i++)
{
if (i % 2 == 0)
Console.WriteLine(i);
}
}
}
Logic
Side-effects
Speaker: Alexey Golub @Tyrrrz
public static class Program
{
public static IEnumerable<int> EnumerateEvenNumbers(int max)
=> Enumerable.Range(1, max).Where(i => i % 2 == 0);
public static void Main(string[] args)
{
var max = int.Parse(Console.ReadLine());
foreach (var number in EnumerateEvenNumbers(max))
Console.WriteLine(number);
}
}
Logic
Side-effects
Speaker: Alexey Golub @Tyrrrz
public static class Program
{
public static TOut Pipe<TIn, TOut>(this TIn in, Func<TIn, TOut> transform)
=> transform(in);
public static IEnumerable<int> EnumerateEvenNumbers(int max)
=> Enumerable.Range(1, max).Where(i => i % 2 == 0);
public static void Main(string[] args)
{
Console.ReadLine()
.Pipe(int.Parse)
.Pipe(EnumerateEvenNumbers)
.ToList()
.ForEach(Console.WriteLine);
}
}
Speaker: Alexey Golub @Tyrrrz
private readonly ICounterpartyRepository _counterpartyRepository;
private readonly ILogger _logger;
/* ... */
public async Task<Counterparty> GetCounterpartyAsync(Transaction transaction)
{
var counterparties = await _counterpartyRepository.GetAll().ToArrayAsync();
foreach (var counterparty in counterparties)
{
if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type))
continue;
if (!counterparty.SupportedCurrencies.Contains(transaction.Currency))
continue;
_logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'");
return counterparty;
}
throw new CounterpartyNotFoundException("No counterparty found to execute this transaction.");
}
Speaker: Alexey Golub @Tyrrrz
public static async Task<Counterparty> GetCounterpartyAsync(
ICounterpartyRepository counterpartyRepository, ILogger logger, Transaction transaction)
{
var counterparties = await counterpartyRepository.GetAll().ToArrayAsync();
foreach (var counterparty in counterparties)
{
if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type))
continue;
if (!counterparty.SupportedCurrencies.Contains(transaction.Currency))
continue;
logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'");
return counterparty;
}
throw new CounterpartyNotFoundException("No counterparty found to execute this transaction.");
}
Speaker: Alexey Golub @Tyrrrz
public delegate Task<IReadOnlyList<Counterparty>> AsyncCounterpartyResolver();
public delegate void LogHandler(string message);
public static async Task<Counterparty> GetCounterpartyAsync(
AsyncCounterpartyResolver getCounterpartiesAsync,
LogHandler log,
Transaction transaction)
{
var counterparties = await getCounterpartiesAsync();
foreach (var counterparty in counterparties)
{
if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type))
continue;
if (!counterparty.SupportedCurrencies.Contains(transaction.Currency))
continue;
log($"Transaction {transaction.Id} routed to '{counterparty}'");
return counterparty;
}
throw new CounterpartyNotFoundException("No counterparty found to execute this transaction.");
}
Speaker: Alexey Golub @Tyrrrz
public static Counterparty GetCounterparty(
IReadOnlyList<Counterparty> counterparties, LogHandler log, Transaction transaction)
{
foreach (var counterparty in counterparties)
{
if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type))
continue;
if (!counterparty.SupportedCurrencies.Contains(transaction.Currency))
continue;
log($"Transaction {transaction.Id} routed to '{counterparty}'");
return counterparty;
}
throw new CounterpartyNotFoundException("No counterparty found to execute this transaction.");
}
Speaker: Alexey Golub @Tyrrrz
public static Counterparty GetCounterparty(
IReadOnlyList<Counterparty> counterparties, Transaction transaction)
{
foreach (var counterparty in counterparties)
{
if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type))
continue;
if (!counterparty.SupportedCurrencies.Contains(transaction.Currency))
continue;
return counterparty;
}
throw new CounterpartyNotFoundException("No counterparty found to execute this transaction.");
}
Speaker: Alexey Golub @Tyrrrz
public static Counterparty? TryGetCounterparty(
IReadOnlyList<Counterparty> counterparties, Transaction transaction)
{
foreach (var counterparty in counterparties)
{
if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type))
continue;
if (!counterparty.SupportedCurrencies.Contains(transaction.Currency))
continue;
return counterparty;
}
return null;
}
Speaker: Alexey Golub @Tyrrrz
public static IEnumerable<Counterparty> GetAvailableCounterparties(
IEnumerable<Counterparty> counterparties, Transaction transaction)
{
return counterparties
.Where(c => c.SupportedTransactionTypes.Contains(transaction.Type))
.Where(c => c.SupportedCurrencies.Contains(transaction.Currency))
}
Speaker: Alexey Golub @Tyrrrz
private readonly ICounterpartyRepository _counterpartyRepository;
private readonly ILogger _logger;
/* ... */
public async Task<Counterparty> GetCounterpartyAsync(Transaction transaction)
{
var counterparties = await _counterpartyRepository.GetAll().ToArrayAsync();
var counterparty = CounterpartyLogic
.GetAvailableCounterparties(counterparties, transaction)
.FirstOrDefault();
_logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'");
return counterparty ??
throw new CounterpartyNotFoundException("No counterparty found to execute this transaction.");
}
Speaker: Alexey Golub @Tyrrrz
[HttpGet]
public async Task<IActionResult> GetCounterparty(Transaction transaction)
{
var counterparties = await _dbContext.Counterparties.ToArrayAsync();
var counterparty = CounterpartyLogic
.GetAvailableCounterparties(counterparties, transactions)
.FirstOrDefault();
_logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'");
if (counterparty is null)
return NotFound("No counterparty found to execute this transaction.");
return Ok(counterparty);
}
Speaker: Alexey Golub @Tyrrrz
Speaker: Alexey Golub @Tyrrrz
Controller
Speaker: Alexey Golub @Tyrrrz
DbContext
Controller
Pull
data
Filter
counterparties
Select
counterparty
Calculate
fee
Deduct
balance
Push
data
… …
Pure-impure segregation principle
• Impure functions can call pure functions
• Pure functions cannot call impure functions
• Impure functions should be pushed outwards
• Work towards maximum purity
Speaker: Alexey Golub @Tyrrrz
Speaker: Alexey Golub @Tyrrrz
Pure layer
Impure layer
System boundary
Testing
Speaker: Alexey Golub @Tyrrrz
Speaker: Alexey Golub @Tyrrrz
Unit tests
Functional tests
Integration
tests
Speaker: Alexey Golub @Tyrrrz
Functional tests
Unit tests
Integration
tests
Speaker: Alexey Golub @Tyrrrz
Functional tests
Logic tests
Integration
tests
Recipe for reliable tests
1. Cover functional requirements (Functional tests)
2. Cover business logic (Logic tests)
3. Cover system-wide integration (Integration tests)
Speaker: Alexey Golub @Tyrrrz
Functional tests shouldn’t be difficult
• TestServer (ASP.NET Core)
• WebApplicationFactory (ASP.NET Core)
• Browser (NancyFx)
• TestConsole (System.CommandLine)
• VirtualConsole (CliFx)
• Docker (*anything*)
Speaker: Alexey Golub @Tyrrrz
Summary
• Avoid introducing dependencies
• Avoid meaningless abstractions
• Avoid tests that rely on mocks
• Avoid cargo cult programming
• Prefer pure-impure segregation
• Prefer pure functions for business logic
• Prefer pipelines to hierarchies
• Prefer functional tests
Speaker: Alexey Golub @Tyrrrz
Consider checking out
• Functional architecture by Mark Seemann
• Async injection by Mark Seemann
• Test-induced damage by David Heinemeier Hansson
• TDD is dead, long live testing by David Heinemeier Hansson
• Functional principles for OOD by Jessica Kerr
• Railway-oriented programming by Scott Wlaschin
Speaker: Alexey Golub @Tyrrrz
Thank you!
Speaker: Alexey Golub @Tyrrrz

More Related Content

PDF
Distributed Eventing in OSGi - Carsten Ziegeler
PDF
Apache Sling - Distributed Eventing, Discovery, and Jobs (adaptTo 2013)
PDF
Forgive me for i have allocated
PDF
AWS Java SDK @ scale
PDF
Android dev 3
PDF
Async - react, don't wait - PingConf
PDF
JSLT: JSON querying and transformation
PPT
{{more}} Kibana4
Distributed Eventing in OSGi - Carsten Ziegeler
Apache Sling - Distributed Eventing, Discovery, and Jobs (adaptTo 2013)
Forgive me for i have allocated
AWS Java SDK @ scale
Android dev 3
Async - react, don't wait - PingConf
JSLT: JSON querying and transformation
{{more}} Kibana4

What's hot (20)

PDF
Data collection in AWS at Schibsted
PDF
Distributed Real-Time Stream Processing: Why and How 2.0
PDF
Reactive Programming with Rx
PDF
Journée DevOps : Des dashboards pour tous avec ElasticSearch, Logstash et Kibana
PPTX
Dapper performance
PDF
Logstash-Elasticsearch-Kibana
PPTX
Cloud Dataflow - A Unified Model for Batch and Streaming Data Processing
PPTX
Presto overview
PDF
Compliance as Code with terraform-compliance
PDF
"ClojureScript journey: from little script, to CLI program, to AWS Lambda fun...
PDF
Docker Logging and analysing with Elastic Stack
PDF
Cassandra Summit EU 2014 - Testing Cassandra Applications
KEY
OpenStack APIs: Present and Future (Beta Talk)
PPTX
Elk with Openstack
PPTX
Elk stack
PDF
Reactive Streams: Handling Data-Flow the Reactive Way
PDF
Machine Learning in a Twitter ETL using ELK
PPTX
PDF
Realtime Data Analytics
PDF
Async – react, don't wait
Data collection in AWS at Schibsted
Distributed Real-Time Stream Processing: Why and How 2.0
Reactive Programming with Rx
Journée DevOps : Des dashboards pour tous avec ElasticSearch, Logstash et Kibana
Dapper performance
Logstash-Elasticsearch-Kibana
Cloud Dataflow - A Unified Model for Batch and Streaming Data Processing
Presto overview
Compliance as Code with terraform-compliance
"ClojureScript journey: from little script, to CLI program, to AWS Lambda fun...
Docker Logging and analysing with Elastic Stack
Cassandra Summit EU 2014 - Testing Cassandra Applications
OpenStack APIs: Present and Future (Beta Talk)
Elk with Openstack
Elk stack
Reactive Streams: Handling Data-Flow the Reactive Way
Machine Learning in a Twitter ETL using ELK
Realtime Data Analytics
Async – react, don't wait
Ad

Similar to Alexey Golub - Dependency absolution (application as a pipeline) | Svitla Smart Talks (20)

PDF
GitHub Actions in action
PDF
DevOps Fest 2020. Alexey Golub. GitHub Actions in action
PDF
Fallacies of unit testing
PDF
Presto anatomy
PPTX
Annotation processing tool
PPTX
5 x HTML5 worth using in APEX (5)
PDF
Akka with Scala
PDF
Gojko Adzic Cucumber
KEY
CouchDB on Android
PDF
Nodejs - A quick tour (v4)
PPTX
Mobile Developers Talks: Delve Mobile
PDF
Oleksii Holub "Expression trees in C#"
PDF
Microservices and modularity with java
PPTX
Event-driven IO server-side JavaScript environment based on V8 Engine
PDF
Expression trees in C#
PDF
Reactive programming on Android
PDF
"Enabling Googley microservices with gRPC" VoxxedDays Minsk edition
PPTX
NET Systems Programming Learned the Hard Way.pptx
PPTX
REST meets Semantic Web
PDF
What to expect from Java 9
GitHub Actions in action
DevOps Fest 2020. Alexey Golub. GitHub Actions in action
Fallacies of unit testing
Presto anatomy
Annotation processing tool
5 x HTML5 worth using in APEX (5)
Akka with Scala
Gojko Adzic Cucumber
CouchDB on Android
Nodejs - A quick tour (v4)
Mobile Developers Talks: Delve Mobile
Oleksii Holub "Expression trees in C#"
Microservices and modularity with java
Event-driven IO server-side JavaScript environment based on V8 Engine
Expression trees in C#
Reactive programming on Android
"Enabling Googley microservices with gRPC" VoxxedDays Minsk edition
NET Systems Programming Learned the Hard Way.pptx
REST meets Semantic Web
What to expect from Java 9
Ad

Recently uploaded (20)

PDF
How UI/UX Design Impacts User Retention in Mobile Apps.pdf
PDF
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
PDF
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton
PPTX
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
PDF
Electronic commerce courselecture one. Pdf
PDF
Encapsulation_ Review paper, used for researhc scholars
PDF
The Rise and Fall of 3GPP – Time for a Sabbatical?
PDF
Empathic Computing: Creating Shared Understanding
PDF
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
PPTX
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
PDF
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
PDF
Reach Out and Touch Someone: Haptics and Empathic Computing
DOCX
The AUB Centre for AI in Media Proposal.docx
PDF
Encapsulation theory and applications.pdf
PPTX
sap open course for s4hana steps from ECC to s4
PDF
7 ChatGPT Prompts to Help You Define Your Ideal Customer Profile.pdf
PDF
KodekX | Application Modernization Development
PDF
Network Security Unit 5.pdf for BCA BBA.
PDF
Chapter 3 Spatial Domain Image Processing.pdf
PDF
NewMind AI Weekly Chronicles - August'25 Week I
How UI/UX Design Impacts User Retention in Mobile Apps.pdf
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
Electronic commerce courselecture one. Pdf
Encapsulation_ Review paper, used for researhc scholars
The Rise and Fall of 3GPP – Time for a Sabbatical?
Empathic Computing: Creating Shared Understanding
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
Reach Out and Touch Someone: Haptics and Empathic Computing
The AUB Centre for AI in Media Proposal.docx
Encapsulation theory and applications.pdf
sap open course for s4hana steps from ECC to s4
7 ChatGPT Prompts to Help You Define Your Ideal Customer Profile.pdf
KodekX | Application Modernization Development
Network Security Unit 5.pdf for BCA BBA.
Chapter 3 Spatial Domain Image Processing.pdf
NewMind AI Weekly Chronicles - August'25 Week I

Alexey Golub - Dependency absolution (application as a pipeline) | Svitla Smart Talks

  • 1. Speaker: Alexey Golub @Tyrrrz Dependency absolution Application as a pipeline
  • 2. Who am I? Speaker: Alexey Golub @Tyrrrz Open source developer • https://github.com/Tyrrrz • 6 active projects • 4000 total stars • 400k total downloads Senior software developer • Svitla Systems, Inc • C#, ASP.NET Core, Docker, AWS Interests • C#, F#, JavaScript • Functional programming • Language design, parsing, DSLs • Product design, photography
  • 3. Best code is no code at all Bad design decisions lead to writing more code in the long run Speaker: Alexey Golub @Tyrrrz
  • 5. Object-oriented approach Speaker: Alexey Golub @Tyrrrz public class Ledger { public Guid Id { get; } public decimal Balance { get; private set; } public Ledger(Guid id, decimal initialBalance) { /* ... */ } public void Debit(decimal amount) { /* ... */ } public void Credit(decimal amount) { /* ... */ } public static Ledger Create(decimal initialBalance = 0.0M) { /* ... */ } public static Ledger Retrieve(Guid id) { /* ... */ } }
  • 6. public void Debit(decimal amount) { if (Balance < amount) throw new InsufficientFundsException(); Balance -= amount; Database.Save(this); } Speaker: Alexey Golub @Tyrrrz Business logic Dependency
  • 7. Speaker: Alexey Golub @Tyrrrz Object Dependency
  • 8. Speaker: Alexey Golub @Tyrrrz Object Dependency Object Abstraction Dependency Mock
  • 9. public void Debit(decimal amount) { if (Balance < amount) throw new InsufficientFundsException(); Balance -= amount; ServiceLocator.GetService<IDatabase>().Save(this); } Speaker: Alexey Golub @Tyrrrz
  • 10. public void Debit(decimal amount) { if (Balance < amount) throw new InsufficientFundsException(); Balance -= amount; ServiceLocator.GetService<IDatabase>().Save(this); } private readonly IDatabase _database; /* ... */ public void Debit(decimal amount) { if (Balance < amount) throw new InsufficientFundsException(); Balance -= amount; _database.Save(this); } Speaker: Alexey Golub @Tyrrrz Abstract dependency
  • 11. Benefits of dependency injection • Inversion of control • Lifetime management • Decoupled implementations • Reduced module complexity • Isolated testing Speaker: Alexey Golub @Tyrrrz
  • 13. Speaker: Alexey Golub @Tyrrrz Domain models Data access Services System boundary
  • 14. Layered/onion architecture public class Ledger { public Guid Id { get; set; } public decimal Amount { get; set; } } public interface ILedgerRepository { Task AddAsync(Ledger ledger); Task UpdateAsync(Ledger ledger); Task<Ledger> RetrieveAsync(Guid id); } public interface ILedgerService { Task<Ledger> CreateAsync(decimal initialBalance = 0.0M); Task<Ledger> RetrieveAsync(Guid id); Task DebitAsync(Guid id, decimal amount); Task CreditAsync(Guid id, decimal amount); } Speaker: Alexey Golub @Tyrrrz
  • 15. Speaker: Alexey Golub @Tyrrrz Ledger LedgerRepository LedgerService LedgerController System boundary (entry point) Service layer Data access layer Domain model
  • 16. Speaker: Alexey Golub @Tyrrrz Ledger LedgerRepository LedgerService LedgerController TransactionFee Counterparty TransactionController TransactionService FeeService Statement StatementService CounterpartyService FeeRepository TransactionRepositoryCounterpartyRepository StatementRepository ScheduledJob NotificationService
  • 17. Layers of issues Speaker: Alexey Golub @Tyrrrz
  • 18. Hard to combine with OOD • Existing paradigms became anti-patterns • Encapsulation is gone • Inheritance is disfavored • Static classes & methods are “untestable” Speaker: Alexey Golub @Tyrrrz
  • 19. Causational indirectness • Hard to trace • Hard to reason about • Implicit dependencies & DI made us lazy Speaker: Alexey Golub @Tyrrrz
  • 20. Leaky async • Async stems from IO side-effects and leaks into business logic • Unnecessary state machines impact performance • Code becomes redundantly verbose Speaker: Alexey Golub @Tyrrrz
  • 21. public class LedgerService : ILedgerService { private readonly ILedgerRepository _repository; /* ... */ public async Task DebitAsync(Guid ledgerId, decimal amount) { var ledger = await _repository.GetByIdAsync(ledgerId); if (ledger is null) throw new EntityNotFoundException(); if (ledger.Balance < amount) throw new InsufficientFundsException(); ledger.Balance -= amount; await _repository.SaveChangesAsync(); } } Speaker: Alexey Golub @Tyrrrz Leaky async
  • 22. public interface ILedgerService { Task<Ledger> CreateAsync(decimal initialBalance = 0.0M); Task<Ledger> RetrieveAsync(Guid id); Task DebitAsync(Guid id, decimal amount); Task CreditAsync(Guid id, decimal amount); } Speaker: Alexey Golub @Tyrrrz ILedgerService reveals the fact that LedgerService depends on ILedgerRepository
  • 23. Obscured complexity • Module complexity is reduced • Total complexity is increased • Assumptions between communicating modules Speaker: Alexey Golub @Tyrrrz
  • 24. Mock-based testing • Implementation-aware • Very brittle • Massive time sink Speaker: Alexey Golub @Tyrrrz
  • 25. // Arrange var transactionRepositoryMock = new Mock<ITransactionRepository>(); transactionRepositoryMock.Setup(x => x.GetAll()) .Returns(testData.AsQueryable()); var counterpartyServiceMock = new Mock<ICounterpartyService>(); counterpartyServiceMock.Setup(x => x.GetCounterpartyAsync(It.IsAny<Transaction>())) .ReturnsAsync(testCounterparty); var transactionService = new TransactionService( transactionRepositoryMock.Object, counterpartyServiceMock.Object ); var transaction = new Transaction { // ... } // Act await transactionService.ExecuteTransactionAsync(transaction); // Assert // ... Speaker: Alexey Golub @Tyrrrz Implementation-aware
  • 26. Autotelic abstractions • Every object requires an explicit abstraction • Abstractions are needed for the sole purpose of mocking • Abstractions don’t try to encapsulate behavior • Abstractions are owned by implementations instead of consumers Speaker: Alexey Golub @Tyrrrz
  • 27. Abstraction is a great tool and a terrible goal Speaker: Alexey Golub @Tyrrrz
  • 28. Is OOP the wrong tool for the job? Speaker: Alexey Golub @Tyrrrz
  • 31. Side-effects Data transformation Speaker: Alexey Golub @Tyrrrz Validating, parsing, mapping, ordering, filtering, projecting… Reading request, writing to DB, enqueuing messages…
  • 35. Speaker: Alexey Golub @Tyrrrz F(x)Data in Data out Isolation SQL
  • 37. Speaker: Alexey Golub @Tyrrrz var fees = _invoiceService.CalculateFees(); Can I make any assumptions about _invoiceService?
  • 38. Speaker: Alexey Golub @Tyrrrz var fees = InvoiceLogic.CalculateFees(ledger, invoiceDate); Dependencies are clear which makes the intent clear
  • 40. private readonly CommonDbContext _dbContext; /* ... */ public IEnumerable<Transaction> GetCompletedTransactions(Guid ledgerId) { return _dbContext.Transactions .Where(t => t.LedgerId == ledgerId) .Where(t => t.IsCompleted); } Speaker: Alexey Golub @Tyrrrz Data container
  • 41. Speaker: Alexey Golub @Tyrrrz public static IEnumerable<Transaction> GetCompletedTransactions( IEnumerable<Transaction> allTransactions, Guid ledgerId) { return allTransactions .Where(t => t.LedgerId == ledgerId) .Where(t => t.IsCompleted); }
  • 43. public static DateTimeOffset GetInvoiceDate(int day) { var instant = DateTimeOffset.Now; var potentialInvoiceDate = instant.Day >= day ? instant.AddDays(day - instant.Day) : instant.AddMonths(-1).AddDays(day - instant.Day); if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Saturday) potentialInvoiceDate = potentialInvoiceDate.AddDays(-1); else if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Sunday) potentialInvoiceDate = potentialInvoiceDate.AddDays(-2); return potentialInvoiceDate; } Non-deterministic Speaker: Alexey Golub @Tyrrrz
  • 44. public static DateTimeOffset GetInvoiceDate(DateTimeOffset instant, int day) { var potentialInvoiceDate = instant.Day >= day ? instant.AddDays(day - instant.Day) : instant.AddMonths(-1).AddDays(day - instant.Day); if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Saturday) potentialInvoiceDate = potentialInvoiceDate.AddDays(-1); else if (potentialInvoiceDate.DayOfWeek == DayOfWeek.Sunday) potentialInvoiceDate = potentialInvoiceDate.AddDays(-2); return potentialInvoiceDate; } Speaker: Alexey Golub @Tyrrrz
  • 46. var result = InvoiceLogic.GetInvoiceDate(instant, day); Speaker: Alexey Golub @Tyrrrz
  • 47. private static IEnumerable<TestCaseData> GetTestCases() { yield return new TestCaseData( new DateTimeOffset(2019, 12, 05, 00, 00, 00, TimeSpan.Zero), 20, new DateTimeOffset(2019, 11, 20, 00, 00, 00, TimeSpan.Zero) ); yield return new TestCaseData( new DateTimeOffset(2019, 12, 25, 00, 00, 00, TimeSpan.Zero), 22, new DateTimeOffset(2019, 12, 20, 00, 00, 00, TimeSpan.Zero) ); } [Test] [TestCaseSource(nameof(GetTestCases))] public void GetInvoiceDate_Test(DateTimeOffset instant, int day, DateTimeOffset expectedResult) { // Act var actualResult = InvoiceLogic.GetInvoiceDate(instant, day); // Assert Assert.That(actualResult, Is.EqualTo(expectedResult)); } Speaker: Alexey Golub @Tyrrrz
  • 49. public static class Program { public static void Main(string[] args) { var max = int.Parse(Console.ReadLine()); for (var i = 1; i <= max; i++) { if (i % 2 == 0) Console.WriteLine(i); } } } Logic Side-effects Speaker: Alexey Golub @Tyrrrz
  • 50. public static class Program { public static IEnumerable<int> EnumerateEvenNumbers(int max) => Enumerable.Range(1, max).Where(i => i % 2 == 0); public static void Main(string[] args) { var max = int.Parse(Console.ReadLine()); foreach (var number in EnumerateEvenNumbers(max)) Console.WriteLine(number); } } Logic Side-effects Speaker: Alexey Golub @Tyrrrz
  • 51. public static class Program { public static TOut Pipe<TIn, TOut>(this TIn in, Func<TIn, TOut> transform) => transform(in); public static IEnumerable<int> EnumerateEvenNumbers(int max) => Enumerable.Range(1, max).Where(i => i % 2 == 0); public static void Main(string[] args) { Console.ReadLine() .Pipe(int.Parse) .Pipe(EnumerateEvenNumbers) .ToList() .ForEach(Console.WriteLine); } } Speaker: Alexey Golub @Tyrrrz
  • 52. private readonly ICounterpartyRepository _counterpartyRepository; private readonly ILogger _logger; /* ... */ public async Task<Counterparty> GetCounterpartyAsync(Transaction transaction) { var counterparties = await _counterpartyRepository.GetAll().ToArrayAsync(); foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; _logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'"); return counterparty; } throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  • 53. public static async Task<Counterparty> GetCounterpartyAsync( ICounterpartyRepository counterpartyRepository, ILogger logger, Transaction transaction) { var counterparties = await counterpartyRepository.GetAll().ToArrayAsync(); foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'"); return counterparty; } throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  • 54. public delegate Task<IReadOnlyList<Counterparty>> AsyncCounterpartyResolver(); public delegate void LogHandler(string message); public static async Task<Counterparty> GetCounterpartyAsync( AsyncCounterpartyResolver getCounterpartiesAsync, LogHandler log, Transaction transaction) { var counterparties = await getCounterpartiesAsync(); foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; log($"Transaction {transaction.Id} routed to '{counterparty}'"); return counterparty; } throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  • 55. public static Counterparty GetCounterparty( IReadOnlyList<Counterparty> counterparties, LogHandler log, Transaction transaction) { foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; log($"Transaction {transaction.Id} routed to '{counterparty}'"); return counterparty; } throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  • 56. public static Counterparty GetCounterparty( IReadOnlyList<Counterparty> counterparties, Transaction transaction) { foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; return counterparty; } throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  • 57. public static Counterparty? TryGetCounterparty( IReadOnlyList<Counterparty> counterparties, Transaction transaction) { foreach (var counterparty in counterparties) { if (!counterparty.SupportedTransactionTypes.Contains(transaction.Type)) continue; if (!counterparty.SupportedCurrencies.Contains(transaction.Currency)) continue; return counterparty; } return null; } Speaker: Alexey Golub @Tyrrrz
  • 58. public static IEnumerable<Counterparty> GetAvailableCounterparties( IEnumerable<Counterparty> counterparties, Transaction transaction) { return counterparties .Where(c => c.SupportedTransactionTypes.Contains(transaction.Type)) .Where(c => c.SupportedCurrencies.Contains(transaction.Currency)) } Speaker: Alexey Golub @Tyrrrz
  • 59. private readonly ICounterpartyRepository _counterpartyRepository; private readonly ILogger _logger; /* ... */ public async Task<Counterparty> GetCounterpartyAsync(Transaction transaction) { var counterparties = await _counterpartyRepository.GetAll().ToArrayAsync(); var counterparty = CounterpartyLogic .GetAvailableCounterparties(counterparties, transaction) .FirstOrDefault(); _logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'"); return counterparty ?? throw new CounterpartyNotFoundException("No counterparty found to execute this transaction."); } Speaker: Alexey Golub @Tyrrrz
  • 60. [HttpGet] public async Task<IActionResult> GetCounterparty(Transaction transaction) { var counterparties = await _dbContext.Counterparties.ToArrayAsync(); var counterparty = CounterpartyLogic .GetAvailableCounterparties(counterparties, transactions) .FirstOrDefault(); _logger.LogInformation($"Transaction {transaction.Id} routed to '{counterparty}'"); if (counterparty is null) return NotFound("No counterparty found to execute this transaction."); return Ok(counterparty); } Speaker: Alexey Golub @Tyrrrz
  • 61. Speaker: Alexey Golub @Tyrrrz Controller
  • 62. Speaker: Alexey Golub @Tyrrrz DbContext Controller Pull data Filter counterparties Select counterparty Calculate fee Deduct balance Push data … …
  • 63. Pure-impure segregation principle • Impure functions can call pure functions • Pure functions cannot call impure functions • Impure functions should be pushed outwards • Work towards maximum purity Speaker: Alexey Golub @Tyrrrz
  • 64. Speaker: Alexey Golub @Tyrrrz Pure layer Impure layer System boundary
  • 66. Speaker: Alexey Golub @Tyrrrz Unit tests Functional tests Integration tests
  • 67. Speaker: Alexey Golub @Tyrrrz Functional tests Unit tests Integration tests
  • 68. Speaker: Alexey Golub @Tyrrrz Functional tests Logic tests Integration tests
  • 69. Recipe for reliable tests 1. Cover functional requirements (Functional tests) 2. Cover business logic (Logic tests) 3. Cover system-wide integration (Integration tests) Speaker: Alexey Golub @Tyrrrz
  • 70. Functional tests shouldn’t be difficult • TestServer (ASP.NET Core) • WebApplicationFactory (ASP.NET Core) • Browser (NancyFx) • TestConsole (System.CommandLine) • VirtualConsole (CliFx) • Docker (*anything*) Speaker: Alexey Golub @Tyrrrz
  • 71. Summary • Avoid introducing dependencies • Avoid meaningless abstractions • Avoid tests that rely on mocks • Avoid cargo cult programming • Prefer pure-impure segregation • Prefer pure functions for business logic • Prefer pipelines to hierarchies • Prefer functional tests Speaker: Alexey Golub @Tyrrrz
  • 72. Consider checking out • Functional architecture by Mark Seemann • Async injection by Mark Seemann • Test-induced damage by David Heinemeier Hansson • TDD is dead, long live testing by David Heinemeier Hansson • Functional principles for OOD by Jessica Kerr • Railway-oriented programming by Scott Wlaschin Speaker: Alexey Golub @Tyrrrz
  • 73. Thank you! Speaker: Alexey Golub @Tyrrrz