[HN Gopher] Maintain a clean architecture in Python with depende...
___________________________________________________________________
Maintain a clean architecture in Python with dependency rules
Author : rekahrv
Score : 115 points
Date : 2022-12-15 13:29 UTC (9 hours ago)
(HTM) web link (sourcery.ai)
(TXT) w3m dump (sourcery.ai)
| vasili111 wrote:
| Am I only person that prefers to use raw SQL over of SQLAlchemy?
| I do not see any real advantage of using SQLAlchemy over raw SQL
| if I do not plan (which I do not plan) in future to switch DB
| engine for the application. Do you see any real advantage of
| using SQLALchemy over raw SQL queries if you do not plan to
| switch DB engine for your application in future?
| willseth wrote:
| It's nice to have all of your db operations in Python and
| automatically integrated with existing Python tooling. It also
| makes it easier to refactor, organize, etc. SQLAlchemy comes
| OOTB with a lot of nice convenience tools and functions, and
| there's an ecosystem built around it, e.g. Alembic for schema
| migration. There are some cases like really complex queries
| where it can get in the way, but overall I find the tradeoffs
| are easily worth it for the convenience
| dontlaugh wrote:
| Alchemy makes it quite easy to compose queries, which isn't
| possible with SQL. That's about it.
| gghhzzgghhzz wrote:
| I use it mostly for reverse engineering a model on top of a
| legacy database when working on projects to clean and migrate
| that data.
|
| I seen some very legacy database 'designs' and have never
| failed to model them with a combination of sqlalchemy join
| mapping, datatype mapping and some object properties in python
| for cases that are simpler to just express as list
| comprehensions.
|
| You end up with some data quality rules / Transformation logic
| you can reasonably share with business users.
|
| On the Load end I normally do that via sql bulk inserts as
| using an ORM just adds too much overhead and not enough
| control.
| lyu07282 wrote:
| Is there something like this for react/jsx? I always wished I
| could constrain component dependencies across the atom > molecule
| > organism layers.
| thundergolfer wrote:
| This is supported in Bazel with package visibility rules. Once
| you've got that feature as a way to tame a larger and expanding
| codebase, you'll wonder why it isn't a feature in more systems.
|
| https://bazel.build/concepts/visibility
| rekahrv wrote:
| Thanks a lot for sharing this link. I haven't used Bazel, but
| this concept of target and load visibility sounds cool.
|
| "Once you've got that feature as a way to tame a larger and
| expanding codebase, you'll wonder why it isn't a feature in
| more systems." :-)
| shankr wrote:
| This has also been recently integrated in pants.
|
| https://github.com/pantsbuild/pants/issues/13393
| hbrn wrote:
| This (plus Law of Demeter) is the right way to handle medium-big
| size projects, though I'm not completely sold on the tooling. I
| mostly do it manually (yes, it is still doable with dozens of
| modules since dependency hierarchy doesn't change often).
|
| One recommendation I have is to present the hierarchy as DAG.
| Existing image
| (https://sourcery.ai/static/05300f06cb847360719e2aa31dc5a31b/...)
| doesn't make it very obvious that api is a highest-level module,
| even though it is clearly stated in the rules.
| AlphaSite wrote:
| Import Linter is probably a better choice for Python since it's
| free https://pypi.org/project/import-linter/
| hbrn wrote:
| I thought about using it at a small scale, but frankly I find
| more value in a visual representation, and once I have that I
| don't want to explicitly blacklist imports: those rules can
| already be derived from the graph (i.e. any import that
| introduces a cycle is a violation).
|
| Doing it manually allows me the following:
|
| 1. I get to define what are the namespaces (domains) that
| matter, irrespective of the package structure. E.g. import
| from stripe.api_resources is still a dependency on stripe,
| not on stripe.api_resources.
|
| 2. Work around a bunch of dependency caveats (frameworks like
| Django do runtime imports and mix high and low level concepts
| in settings, db foreign keys might inverse your dependencies,
| etc)
|
| 3. Violations are very easy to see: they are cycles in the
| graph, i.e. arrows pointing upwards. Those are typically
| design flaws. Though I still allow certain violations because
| practicality beats purity.
|
| 4. Since some violations are allowed, I get to decide how to
| arrange the graph so that it is more clear what is the flaw
| and how to address it.
|
| I haven't found a good tool that allows me to get all of
| these. One day I'll have to build it myself.
| rekahrv wrote:
| Yes, Law of Demeter is exactly what these rules are trying to
| achieve. :-) Thanks, a DAG is a great recommendation.
| neves wrote:
| I've already seem tools like this for other languages, but never
| seen someone effectively using them. Does anyone here has good or
| bad experiences with these architecture rule systems?
| hakanderyal wrote:
| After dealing with that problem and enduring the pain of it for
| years, I finally switched to C#/.NET. It has the necessary
| tooling to achieve this and more.
|
| Rewriting a lot of things was time well spent rather than trying
| to tame the dynamic nature of Python and my tendency to overuse
| it.
|
| And I can't believe I'm writing this after all these years
| evangelizing Python and dynamically typed languages.
| elforce002 wrote:
| Interesting. What about using mypy to have some sort of static
| typing a la typescript?
| wiseowise wrote:
| Lipstick on a pig. Unlike TypeScript, mypy feels really
| clunky.
| vyrotek wrote:
| Pleasantly surprised to see .NET shared on HN! I've had a lot
| of success with it in my career building several SaaS platforms
| from the ground-up. The tooling is great. It's wild how
| productive a small startup team can be on the .NET stack using
| a clean architecture.
| [deleted]
| whiskey14 wrote:
| Can you please give a short summary of why I should give
| C#/.NET a go for my backend services?
|
| I've been fighting battles in Python backend services to get a
| nicely decoupled API, logic and DB layers for a while...but
| sqlalchelmy, alembic and flask/django/fastAPI are my safety
| blankets
| camdenreslink wrote:
| One reason is that entity framework is the best ORM out
| there. It blows sqlalchemy and alembic out of the water imo
| (I've used both a bunch).
|
| Another reason is that decoupling and adding layers to your
| code is more part of the culture. Look up "domain driven
| design C#" or "onion architecture C#" and there will be a lot
| of resources on how to achieve it. There is stuff out there
| for Python as well (and the concepts translate between
| languages), but not nearly as much.
| megaman821 wrote:
| I haven't played with SQLAlchemy in a while, but I was
| comparing EF core to the Django ORM, and EF core seemed to
| be lacking in features. There were a few things missing but
| the two that pop to mind are Window function and Case
| statements.
| mattgreenrocks wrote:
| The .NET ecosystem is great.
|
| It feels a lot more professional than other ecosystems. For
| example, they actually talk about layering/coupling as
| professionals should! People actually seem to talk about
| architecture as well rather than blindly believing that the
| conventions forced on them by a framework are sufficient
| for all use cases.
|
| I especially like the gradient in the .NET world from micro
| ORMs to full-fledged ORMS. Most ecosystems seem to develop
| a big ORM that constantly accrues features (and bugs) and
| eventually becomes enshrined as a "best practice" because
| it acts as a kitchen sink.
| daxfohl wrote:
| +1 I was much happier using Dapper compared to EF. I
| figure if it's good enough to run stackoverflow, it's
| probably good enough for whatever I happen to be doing.
|
| The amount of open source in dotnet is great. (I think
| more than Java? My impression of that is dominated by
| Apache etc., though my experience in the Java ecosystem
| is limited. Presumably people in Java land would expect
| the same of dotnet being dominated by Microsoft, but
| that's _really_ not the case).
| hakanderyal wrote:
| I was in the same boat. I wanted to switch a few years ago
| actually, but EF core was missing core features I took for
| granted in Sqlalchemy.
|
| As for the reasons:
|
| - Static typing and C# projects makes code organization and
| refactoring dead easy.
|
| - Modern C# doesn't require that much boilerplate, and has
| features that allows a developer to speed up development,
| like Python.
|
| - EF Core covers everything I need from SqlAlchemy/Alembic.
|
| - LINQ is an awesome way to work with collections. Type safe
| DB queries comes handy both when developing and refactoring.
|
| - ASP.NET covers everything I need from flask/Fastapi.
|
| - More speed and lower resource usage is nice.
|
| - Being able to use an IDE with it's full power is nice.
|
| My main reason to switch was static typing, and my only
| requirement was a good ORM comparable to SqlAlchemy. The rest
| is just bonus.
| danuker wrote:
| > - More speed and lower resource usage is nice.
|
| > - Being able to use an IDE with it's full power is nice.
|
| In my experience, Visual Studio is much slower when
| developing. My Python workflow affords a 200ms red-green-
| refactor loop, while VS is on the order of several seconds.
|
| This might not seem like much, but it has a great impact on
| my engagement, flow, and satisfaction.
| hakanderyal wrote:
| Rebuilding the project to run the tests adds a bit of
| time, yes. I see this as a cost of the static typing,
| runtime speed etc.
|
| It's worth it in the end. YMMV.
| danuker wrote:
| I am not so sure it's worth it. In general, developer
| time is much more valuable than machine time.
|
| Maybe if you're building a large high-performance server,
| you should invest in performance. But otherwise, if you
| only look at at computational complexity, and batch
| up/avoid I/O when possible, you're fine.
| gmueckl wrote:
| Static typing in .NET saves developer time on a massive
| scale. Sure, the compile times and startup times may be
| longer (and tests may take longer to run for that
| reason), but the languages also allow for editor/IDE
| tooling that boosts developer productivity massively.
| Visual Studio with Resharper or Rider may seem expensive,
| but if you work with these tools full time, they pay for
| their cost multiple times over in almost no time.
| lowbloodsugar wrote:
| I grew up with BASIC and made it to Java by way of
| assembly, C, C++ and C#. This year I put some Rust into
| production tooling. I've used python along the way, but
| usually as a scripting tool. I've never worked at a
| company whose codebase involves a lot of python. So
| beware of confirmation bias in my thinking.
|
| What follows is my opinion, I am aware it is my opinion,
| but in my sphere of influence, it is not up for debate
| when it comes to writing code. It might occasionally be a
| conversation over lunch.
|
| I put python in the same bucket as BASIC. It's not a
| production language. "developer time is much more
| valuable than machine time." Yes. Absolutely. Iteration
| speed is vital. But it is vital in more than just the
| test loop. It is important in the "minor refactoring of
| various classes" up to the "major refactoring of entire
| systems" loop too. And python just doesn't make that
| easy. It actively makes it difficult. It makes
| comprehension difficult. It's difficult to look at python
| code in a code-review and have a good idea of what the
| classes involved are. I don't even write scripts in it
| any more. I've found that any script that is worth
| writing is likely to grow and evolve over time, and if it
| is not written in something like C# or Java, then it will
| become an intolerable mess. I've seen entire
| organizations that are basically cargo culting.
|
| I encourage you to learn a statically typed language and
| its tooling.
| yCombLinks wrote:
| All of the benefits of static typing save a ton of
| developer time in other places. No one is talking about
| saving machine time as the primary benefit of static
| typing.
| hakanderyal wrote:
| For me, runtime performance is only a tiny percent of the
| advantages. It could have been slower than Python, I
| would still make the switch.
| kcartlidge wrote:
| > _Rebuilding the project to run the tests adds a bit of
| time, yes._
|
| It costs money, but I've paid for NCrunch for a fair few
| years and find it invaluable for this reason. It doesn't
| even need you to _save_ changes before it spots them and
| runs affected tests in the background.
|
| If cost is an issue you can also start `dotnet watch
| test` going in a terminal/command prompt for non-
| interactive live-reload testing.
| roflyear wrote:
| This is true. Almost all C# projects will take a while
| for you to run. It is unfortunate.
|
| The upside is hopefully you "don't need to run it as many
| times" but ... eh. No thanks.
| kcartlidge wrote:
| There is a delay, true. Inevitable with the compilation
| phase, and I _do_ find it irritating that my Go stuff
| builds so much faster. That said, there 's reasonable
| (not perfect) live reloading happening these days which
| helps somewhat.
| dfee wrote:
| My journey went through mypy, then typescript for frontend,
| the typescript on node. The story here was the type system is
| so much better that it allowed better prototyping, larger
| codebases and confidence.
|
| I've done a lot of C# and Java now over the last few years,
| and I don't love their type system, esp compared to
| typescript, but they scale much better against large
| codebases - especially with tooling like bazel.
|
| I've been looking at Haskell and Rust a lot to fill this
| intermediary: code that's performant, with a very expressive
| type system.
|
| I maintain(ed) a number of popular python packages, and that
| journey lasted for nearly a decade.
| daxfohl wrote:
| This is my experience, having gone the route you're
| looking. Haskell (~6 month trial) was unproductive for me.
| Primarily the ecosystem is full of abandonware. Secondarily
| it lures you into spending _way_ too much time refactoring
| stuff into the most concise possible form, which you can no
| longer understand (and frequently needs rewritten
| completely because the tiniest change to the most concise
| possible form invariably explodes through several layers
| when you have to make changes later). Rust (~3 month trial)
| may be great for codebases where you 'd legitimately
| consider C / C++, but too much work otherwise; I personally
| wasn't doing anything that I'd use C for, so it was not
| worth it.
|
| I ended up being very happy with F# as a middle ground for
| several years, but eventually migrated back to C# as they
| started adding more and more F# features. The primary
| challenge with F# was the parity mismatch with the
| underlying runtime, so you end up having to write a fair
| amount of non-idiomatic F# to interop with common
| libraries. But otherwise it's great. (I also tried Scala
| for a year and hated it: too many ways to do any one
| thing).
| electroly wrote:
| Other commenters have good specific points but I'll add one
| overarching theme: .NET is developed by a well-funded
| corporation that is incentivized to bring all the popular
| innovations from other ecosystems back to the .NET world in a
| cohesive form. If something becomes popular in another
| programming ecosystem and people want it, we'll get it in
| .NET and it'll be done in the same style as everything else
| we have. It's pretty refreshing working with a system that
| was designed to work together rather than cobbling bits
| together.
| andrew_eu wrote:
| Before clicking on this, I expected to see import-linter [0]
| which achieves something very similar but with, in my opinion, a
| bit less magic. Another solution in a similar spirit is Pants
| [1], though this is actually a build system which allows you to
| constrain dependencies between different artifacts (e.g. which
| modules are allowed to depend on which modules).
|
| To Sourcery's credit, their product looks much more in the realm
| of "developer experience" -- closer to Copilot (or what I
| understand of it) than to import-linter. Props to them for at
| least having a page about security [2] and building a solution
| which doesn't inherently require all of your source code to be
| shared with a vendor's server.
|
| [0] https://github.com/seddonym/import-linter
|
| [1] https://www.pantsbuild.org/
|
| [2] https://docs.sourcery.ai/Product/Permissions-and-Security/
| memco wrote:
| Thanks for the additional tools to tackle this problem. We
| usually don't have problems with this at work, but I just so
| happened to discover one today and was dreading the work it
| will take to sort out how to fix it.
| revskill wrote:
| In Typescript, i normally just allow interface to be dependencies
| between layers. (API, command line programs,..) -> (Services) ->
| (Database).
| Thaxll wrote:
| Your DB / api layer should never touch the same models.
| rekahrv wrote:
| Thanks, that's a good point and perhaps a good topic for a
| future post :-) How to ensure that the API and the db use
| different models even if those models are in the same package?
| dangets wrote:
| I struggle with this also, I assume the answer is to not have
| them in the same package. You can also break the application
| into separate 'domain', 'infra', 'application' modules as
| documented in [0] with rules on what dependencies are allowed
| in each module (e.g. domain should not have db or
| serialization implementation). The problem is that this does
| create several adapter layers which adds to the mental
| complexity.
|
| [0] https://learn.microsoft.com/en-
| us/dotnet/architecture/micros...
| marginalia_nu wrote:
| Why not?
| aobdev wrote:
| I hate that these are called models (probably because they
| extend pydantic's BaseModel), but if they were called Schema or
| Serializers would this still be true? Typically what you see in
| a FastAPI project is a class that parsers the request body, and
| the same or slightly modified class that serializes the
| response back out after touching the DB. And this isn't a new
| idea, because Flask+Marshmallow and DRF do the exact same
| thing.
| rekahrv wrote:
| I've used multiple names for similar packages incl. `models`
| and `schemas`. :-) Yes, for this example, I picked `models`
| to follow Pydantic's terminology.
|
| IMO, the FastAPI approach you described makes a lot of sense:
| The "schema" stored in the db and the "schema" returned by
| the API aren't the same, but they are quite similar. They
| have many common properties => They can often have a common
| base class.
| [deleted]
| inwit wrote:
| It's these kind of rules that mean I'm here wading through 5
| layers of exquisitely decoupled nonsense that could be done in
| a few lines
| Thaxll wrote:
| Convert function between API and DB model does not sound
| complicated.
|
| Storing the API model in your DB is really a bad idea.
| camgunz wrote:
| I just listened to the DHH/Kent Beck/Martin Fowler discussion
| about TDD "damage" and both sides still seemed unconvinced by
| the end of it, but this exact example came up. It seems like
| SOA (whether it's DDD or Hexagonal or Clean or w/e) and TDD
| really push you towards this kind of layer bloat for one
| reason or another.
|
| I'm (maybe obviously) on the SOA-skeptic side, my arguments
| generally are:
|
| - Most apps aren't that big and don't need multiple layers of
| abstraction (i.e. the ORM and its models are totally fine).
| If the app starts getting too big for its britches, probably
| the best thing to do is make it 2 apps (too big: 2 apps is a
| good slogan here).
|
| - Dependency injection and mocks are pretty bad ideas that
| are only occasionally useful (DHH uses the example of a
| payments gateway), but mostly push IoC through your whole app
| and make control flow confusingly backwards. Mocks are always
| in disrepair, and almost never accurately reflect what
| they're trying to mock, and thus ironically are big vectors
| for bugs that make it through testing.
|
| - Having tons of unit tests tends to slow eng velocity to a
| crawl, because they test the parts of the application that
| aren't the requirements (were these functions called, what's
| the call signature of this function, was this class
| instantiated, etc.). Unit tests create a super-fine-grained
| shadow spec about the lowest level details of your
| application, and mostly I think they shouldn't ever be
| committed to a repo. They help during individual development,
| but then the whole team is stuck maintaining them forever
| whenever they make changes. They also tend to slow down CI
| because they're slow and always flaky.
|
| - You almost certainly will never need to switch databases,
| let alone abstract across a database, a message queue, and a
| web api. It's not worth doing a "repo" abstraction and
| encapsulating those details.
|
| - There are (now) really good libraries for almost anything
| you want to do. ORMs literally map database entities to
| domain entities--they just abstract the persistence for you.
| Sounds like a repo to me! We also have good validation,
| logging, monitoring, auth/auth etc. built into frameworks and
| 3rd party services. A lot of the things you might put into
| other layers or even other services are now neatly packaged
| into libraries/frameworks you can just use and SaaS things
| you can just buy, leaving you free to just implement your
| business logic.
| kortex wrote:
| Agree on the points that you should never need to abstract
| over your database, orm, message queue, etc.
|
| Disagree on dependency injection. I came from the
| globals/patch everything school of python, to the
| Fastapi/Pytest DI flavor, and it's a breath of fresh air.
| It's just so much easier to abstract the IO providers and
| swap them out with objects tailored to the test suite - eg
| for database, I create db objects which roll back any
| transactions between tests.
|
| Hard disagree on unit tests. Maybe in other languages, but
| in Python, trying to develop even a moderately complex app
| without unit tests is a nightmare. I know, I've lived it.
| Even in an app with >85% unit test coverage, there was
| still a ton of friction around development on any of the
| interfaces which had low coverage.
|
| Any gains in velocity of development almost always cost far
| more in debugging down the road.
|
| I love python, but it is really prone to dumb footguns at
| runtime, NoneType errors in particular. You need to impose
| a lot of discipline to make large python apps enjoyable to
| develop on.
| yunohn wrote:
| > leaving you free to just implement your business logic.
|
| Often, engineers (and HN) forget that code is a means to an
| end - not an artistic expression that provides value by its
| pure existence.
| hbrn wrote:
| I mostly agree with you and DHH on that topic, however in
| my experience reasonably applied SOA/DDD actually shields
| me from this layering nonsense.
|
| When your apps live as a service on the network or as a
| nicely isolated module in your repo, you no longer have a
| reason to over-engineer them. You don't need a grandiose
| architecture that solves every problem, instead you can
| make local decisions that are good enough in the specific
| context. Though, admittedly, I found it hard to sell such
| "inconsistencies" to other tech leaders, most folks aspire
| to those grandiosities.
|
| > If the app starts getting too big for its britches,
| probably the best thing to do is make it 2 apps
|
| That's the argument in favor of SOA, isn't it?
| chao- wrote:
| I generally agree with the position that unit tests should
| be used with discretion, and that full coverage via unit
| tests often leads to thousands of low-ulitility or
| redundant tests, and so on. However I cannot agree with
| this:
|
| > _They also tend to slow down CI because they 're slow and
| always flaky._
|
| In my experience, unit tests are the most stable, the least
| flaky, because they touch the least code and often have
| very simple setup. An integration test might rely on four
| database tables being just-so, and go on to connect with
| two external services (and whether mocked, replayed, or
| live, flakiness may arise). That integration test is twenty
| times more valuable, but it is equally more likely to break
| for reasons tangential to its core assertions.
| yuppiepuppie wrote:
| Title is misleading, it should be "Maintain a clean architecture
| in FastAPI with dependency rules"
| rekahrv wrote:
| Thanks, that's a good point. We thought that a small FastAPI
| project shows the general concept as well. Do you have
| suggestions which other examples would be useful?
| lyu07282 wrote:
| It has nothing to do with FastAPI, pretty sure sourcery would
| work with anything.
| w_t_payne wrote:
| I do exactly this in my side project. I have a set of rules which
| put restrictions on which packages and modules can be included
| from other packages and modules. For example, a high maturity
| package is not allowed to depend upon a low maturity package.
| Similarly, a core library package is not allowed to rely on a
| package that is specific to a particular product or a particular
| piece of bespoke development. In this way, much of the potential
| for circular dependencies is eliminated, and the purpose and
| internet is clearly communicated.
|
| (I don't do this using sourcery though ... I have my own set of
| rules)
| rekahrv wrote:
| That's very cool. Can you tell a bit more about this set of
| rules?
|
| "For example, a high maturity package is not allowed to depend
| upon a low maturity package. Similarly, a core library package
| is not allowed to rely on a package that is specific to a
| particular product or a particular piece of bespoke
| development." I really like these.
| w_t_payne wrote:
| I have a general system for representing metadata in source
| files. (I use YAML documents embedded in block comments).
|
| Some of this metadata gives traceability information for
| requirements, tests etc.. while other metadata enables me to
| associate a maturity level with each file.
|
| My build system understands this metadata and uses it to
| inform e.g. the minimum test coverage that it expects on a
| file-by-file basis.
|
| The same metadata is used to ensure that all of the other
| components that a file references are at the same level of
| maturity or higher.
|
| I also have metadata for each file (partly derived from
| location in the repository) that gives each file a number
| which defines it's position in a hierarchy of design
| elements.
|
| The position in the hierarchy helps to indicate what the
| purpose of the file is. I use this to make a distinction
| between those core, foundational, stable design elements upon
| which other design elements may build, and those more
| peripheral, ephemeral and 'agile' design elements which can
| be quickly tailored to meet the needs of a client or partner.
|
| This means that a (hopefully stable) core API component can
| be prevented from relying upon a (perhaps less stable)
| bespoke customer-specific component. It also means that
| there's more freedom in changing and adapting peripheral
| designs as you can have confidence that it's stability is not
| something that is going to be relied upon.
| rekahrv wrote:
| Thanks for the detailed description. That's a really
| sophisticated system with several cool features. * minimum
| test coverage on a file-by-file basis * various levels
| maturity
|
| "It also means that there's more freedom in changing and
| adapting peripheral designs as you can have confidence that
| it's stability is not something that is going to be relied
| upon." That's a big advantage, indeed.
|
| I also like the concept of storing this metadata next to
| the code in structured comments.
| cjohnson318 wrote:
| I do a similar thing. Here's the style-guide I learned from:
| https://phalt.github.io/django-api-domains/styleguide/
|
| Basically, you have an api class for each Django app, and you
| use this class for all external interactions. The api class
| calls the service class, and the service class deals with the
| Django ORM. I added a view class, which is my
| DjangoRestFramework layer; so when a request comes in, it's
| caught by my view class, and passed onto the api class. I have
| DRF serializers for outgoing data, and pydantic schemas for
| incoming data. I also have a selector class for read-only views
| of my data.
|
| It's a _lot_ of typing, but I know exactly where everything is
| when something goes wrong, or I need to add a small adjustment
| somewhere, also it 's easy for new devs to learn and use. One
| downside is that an api change require you to touch a dozen
| files.
| rekahrv wrote:
| Thanks a lot. The Django API Domains Styleguide looks great.
| Do you perhaps know some open source projects that follow
| this structure?
| mikeholler wrote:
| Is there anything similar to this for Java/Kotlin/Gradle?
| Sankozi wrote:
| There is ArchUnit - https://www.archunit.org/
| mikeholler wrote:
| Thanks, that looks like exactly what I was looking for.
| naizarak wrote:
| JPMS allows for exactly this, but that entire project was so
| poorly implemented that no one uses it.
___________________________________________________________________
(page generated 2022-12-15 23:01 UTC)